Last active
February 27, 2024 11:21
-
-
Save mayoff/ea37ee75a87efab5d7e8 to your computer and use it in GitHub Desktop.
A floating button on iOS for http://stackoverflow.com/q/34777558/77567
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class FloatingButtonController: UIViewController { | |
private(set) var button: UIButton! | |
required init?(coder aDecoder: NSCoder) { | |
fatalError() | |
} | |
init() { | |
super.init(nibName: nil, bundle: nil) | |
window.windowLevel = CGFloat.max | |
window.hidden = false | |
window.rootViewController = self | |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil) | |
} | |
private let window = FloatingButtonWindow() | |
override func loadView() { | |
let view = UIView() | |
let button = UIButton(type: .Custom) | |
button.setTitle("Floating", forState: .Normal) | |
button.setTitleColor(UIColor.greenColor(), forState: .Normal) | |
button.backgroundColor = UIColor.whiteColor() | |
button.layer.shadowColor = UIColor.blackColor().CGColor | |
button.layer.shadowRadius = 3 | |
button.layer.shadowOpacity = 0.8 | |
button.layer.shadowOffset = CGSize.zero | |
button.sizeToFit() | |
button.frame = CGRect(origin: CGPointMake(10, 10), size: button.bounds.size) | |
button.autoresizingMask = [] | |
view.addSubview(button) | |
self.view = view | |
self.button = button | |
window.button = button | |
let panner = UIPanGestureRecognizer(target: self, action: "panDidFire:") | |
button.addGestureRecognizer(panner) | |
} | |
override func viewDidLayoutSubviews() { | |
super.viewDidLayoutSubviews() | |
snapButtonToSocket() | |
} | |
func panDidFire(panner: UIPanGestureRecognizer) { | |
let offset = panner.translationInView(view) | |
panner.setTranslation(CGPoint.zero, inView: view) | |
var center = button.center | |
center.x += offset.x | |
center.y += offset.y | |
button.center = center | |
if panner.state == .Ended || panner.state == .Cancelled { | |
UIView.animateWithDuration(0.3) { | |
self.snapButtonToSocket() | |
} | |
} | |
} | |
func keyboardDidShow(note: NSNotification) { | |
window.windowLevel = 0 | |
window.windowLevel = CGFloat.max | |
} | |
private func snapButtonToSocket() { | |
var bestSocket = CGPoint.zero | |
var distanceToBestSocket = CGFloat.infinity | |
let center = button.center | |
for socket in sockets { | |
let distance = hypot(center.x - socket.x, center.y - socket.y) | |
if distance < distanceToBestSocket { | |
distanceToBestSocket = distance | |
bestSocket = socket | |
} | |
} | |
button.center = bestSocket | |
} | |
private var sockets: [CGPoint] { | |
let buttonSize = button.bounds.size | |
let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2) | |
let sockets: [CGPoint] = [ | |
CGPointMake(rect.minX, rect.minY), | |
CGPointMake(rect.minX, rect.maxY), | |
CGPointMake(rect.maxX, rect.minY), | |
CGPointMake(rect.maxX, rect.maxY), | |
CGPointMake(rect.midX, rect.midY) | |
] | |
return sockets | |
} | |
} | |
private class FloatingButtonWindow: UIWindow { | |
var button: UIButton? | |
init() { | |
super.init(frame: UIScreen.mainScreen().bounds) | |
backgroundColor = nil | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
private override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { | |
guard let button = button else { return false } | |
let buttonPoint = convertPoint(point, toView: button) | |
return button.pointInside(buttonPoint, withEvent: event) | |
} | |
} |
can you please update code for iOS 13 and above when scene delegate is used
import UIKit
class FloatingButtonController: UIViewController {
private(set) var button: UIButton!
required init?(coder aDecoder: NSCoder) {
fatalError()
}
init(scene: UIWindowScene) {
window = FloatingButtonWindow(scene: scene)
super.init(nibName: nil, bundle: nil)
window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
window.isHidden = false
window.rootViewController = self
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow(note:)), name: UIResponder.keyboardDidShowNotification, object: nil)
}
private let window: FloatingButtonWindow
override func loadView() {
let view = UIView()
let button = UIButton(type: .custom)
button.setTitle("Floating", for: .normal)
button.setTitleColor(UIColor.green, for: .normal)
button.backgroundColor = UIColor.white
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowRadius = 3
button.layer.shadowOpacity = 0.8
button.layer.shadowOffset = CGSize.zero
button.sizeToFit()
button.frame = CGRect(origin: CGPoint(x: 10, y: 10), size: button.bounds.size)
button.autoresizingMask = []
view.addSubview(button)
self.view = view
self.button = button
window.button = button
let panner = UIPanGestureRecognizer(target: self, action: #selector(panDidFire(panner:)))
button.addGestureRecognizer(panner)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
snapButtonToSocket()
}
@objc func panDidFire(panner: UIPanGestureRecognizer) {
let offset = panner.translation(in: view)
panner.setTranslation(CGPoint.zero, in: view)
var center = button.center
center.x += offset.x
center.y += offset.y
button.center = center
if panner.state == .ended || panner.state == .cancelled {
UIView.animate(withDuration: 0.3) {
self.snapButtonToSocket()
}
}
}
@objc func keyboardDidShow(note: NSNotification) {
window.windowLevel = UIWindow.Level(rawValue: 0)
window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
}
private func snapButtonToSocket() {
var bestSocket = CGPoint.zero
var distanceToBestSocket = CGFloat.infinity
let center = button.center
for socket in sockets {
let distance = hypot(center.x - socket.x, center.y - socket.y)
if distance < distanceToBestSocket {
distanceToBestSocket = distance
bestSocket = socket
}
}
button.center = bestSocket
}
private var sockets: [CGPoint] {
let buttonSize = button.bounds.size
let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
let sockets: [CGPoint] = [
CGPoint(x: rect.minX, y: rect.minY),
CGPoint(x: rect.minX, y: rect.maxY),
CGPoint(x: rect.maxX, y: rect.minY),
CGPoint(x: rect.maxX, y: rect.maxY),
CGPoint(x: rect.midX, y: rect.midY)
]
return sockets
}
}
private class FloatingButtonWindow: UIWindow {
var button: UIButton?
init(scene: UIWindowScene) {
super.init(windowScene: scene)
backgroundColor = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let button = button else { return false }
let buttonPoint = convert(point, to: button)
return button.point(inside: buttonPoint, with: event)
}
}
in SceneDelegate:
var floatingButtonController: FloatingButtonController?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
floatingButtonController = FloatingButtonController(scene: scene)
floatingButtonController?.button.addTarget(self, action: #selector(floatingButtonWasTapped), for: .touchUpInside)
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
can you please update code for iOS 13 and above when scene delegate is used