No matter if modals are presented or the user performs any type of segue.
Is there a way to keep the button \"always on top\" (not the top of the screen) throughout
In Swift 3:
import UIKit
private class FloatingButtonWindow: UIWindow {
var button: UIButton?
var floatingButtonController: FloatingButtonController?
init() {
super.init(frame: UIScreen.main.bounds)
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)
}
}
class FloatingButtonController: UIViewController {
private(set) var button: UIButton!
private let window = FloatingButtonWindow()
required init?(coder aDecoder: NSCoder) {
fatalError()
}
init() {
super.init(nibName: nil, bundle: nil)
window.windowLevel = CGFloat.greatestFiniteMagnitude
window.isHidden = false
window.rootViewController = self
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
}
func keyboardDidShow(note: NSNotification) {
window.windowLevel = 0
window.windowLevel = CGFloat.greatestFiniteMagnitude
}
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))
button.addGestureRecognizer(panner)
}
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()
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
snapButtonToSocket()
}
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 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
}
}
And in AppDelegate:
var floatingButtonController: FloatingButtonController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
floatingButtonController = FloatingButtonController()
floatingButtonController?.button.addTarget(self, action: #selector(AppDelegate.floatingButtonWasTapped), for: .touchUpInside)
return true
}
func floatingButtonWasTapped() {
let alert = UIAlertController(title: "Warning", message: "Don't do that!", preferredStyle: .alert)
let action = UIAlertAction(title: "Sorry…", style: .default, handler: nil)
alert.addAction(action)
window?.rootViewController?.present(alert, animated: true, completion: nil)
}