In iOS, how do I create a button that is always on top of all other view controllers?

前端 未结 8 1887
悲哀的现实
悲哀的现实 2020-11-28 02:09

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

8条回答
  •  生来不讨喜
    2020-11-28 02:21

    Swift 4.2 - UIViewController extension

    A modified version of Rob Mayoff's answer for local floating buttons.

    UIViewController extension may provide you a better control over floating button. However it will be added on a single view controller which you call addFloatingButton instead of all view controllers..

    • Adding floating button in a specific view controller enables to add view controller specific actions to floating button.
    • It won't block the topViewController function if you use it frequently. (It is provided below)
    • If you use the floating button to toggle support chat for your application(for example Zendesk Chat API), UIViewController extension method provides you a better control over navigation between view controllers.

    For this question, this might not be the best practice however, with this type control over floating button should enable different practices for different purposes.

    UIViewController Extension

    import UIKit
    
    extension UIViewController {
        private struct AssociatedKeys {
            static var floatingButton: UIButton?
        }
    
        var floatingButton: UIButton? {
            get {
                guard let value = objc_getAssociatedObject(self, &AssociatedKeys.floatingButton) as? UIButton else {return nil}
                return value
            }
            set(newValue) {
                objc_setAssociatedObject(self, &AssociatedKeys.floatingButton, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    
        func addFloatingButton() {
            // Customize your own floating button UI
            let button = UIButton(type: .custom)
            let image = UIImage(named: "tab_livesupport_unselected")?.withRenderingMode(.alwaysTemplate)
            button.tintColor = .white
            button.setImage(image, for: .normal)
            button.backgroundColor = UIColor.obiletGreen
            button.layer.shadowColor = UIColor.black.cgColor
            button.layer.shadowRadius = 3
            button.layer.shadowOpacity = 0.12
            button.layer.shadowOffset = CGSize(width: 0, height: 1)
            button.sizeToFit()
            let buttonSize = CGSize(width: 60, height: 60)
            let rect = UIScreen.main.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
            button.frame = CGRect(origin: CGPoint(x: rect.maxX - 15, y: rect.maxY - 50), size: CGSize(width: 60, height: 60))
            // button.cornerRadius = 30 -> Will destroy your shadows, however you can still find workarounds for rounded shadow.
            button.autoresizingMask = []
            view.addSubview(button)
            floatingButton = button
            let panner = UIPanGestureRecognizer(target: self, action: #selector(panDidFire(panner:)))
            floatingButton?.addGestureRecognizer(panner)
            snapButtonToSocket()
        }
    
        @objc fileprivate func panDidFire(panner: UIPanGestureRecognizer) {
            guard let floatingButton = floatingButton else {return}
            let offset = panner.translation(in: view)
            panner.setTranslation(CGPoint.zero, in: view)
            var center = floatingButton.center
            center.x += offset.x
            center.y += offset.y
            floatingButton.center = center
    
            if panner.state == .ended || panner.state == .cancelled {
                UIView.animate(withDuration: 0.3) {
                    self.snapButtonToSocket()
                }
            }
        }
    
        fileprivate func snapButtonToSocket() {
            guard let floatingButton = floatingButton else {return}
            var bestSocket = CGPoint.zero
            var distanceToBestSocket = CGFloat.infinity
            let center = floatingButton.center
            for socket in sockets {
                let distance = hypot(center.x - socket.x, center.y - socket.y)
                if distance < distanceToBestSocket {
                    distanceToBestSocket = distance
                    bestSocket = socket
                }
            }
            floatingButton.center = bestSocket
        }
    
        fileprivate var sockets: [CGPoint] {
            let buttonSize = floatingButton?.bounds.size ?? CGSize(width: 0, height: 0)
            let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
            let sockets: [CGPoint] = [
                CGPoint(x: rect.minX + 15, y: rect.minY + 30),
                CGPoint(x: rect.minX + 15, y: rect.maxY - 50),
                CGPoint(x: rect.maxX - 15, y: rect.minY + 30),
                CGPoint(x: rect.maxX - 15, y: rect.maxY - 50)
            ]
            return sockets
        }
        // Custom socket position to hold Y position and snap to horizontal edges.
        // You can snap to any coordinate on screen by setting custom socket positions.
        fileprivate var horizontalSockets: [CGPoint] {
            guard let floatingButton = floatingButton else {return []}
            let buttonSize = floatingButton.bounds.size
            let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
            let y = min(rect.maxY - 50, max(rect.minY + 30, floatingButton.frame.minY + buttonSize.height / 2))
            let sockets: [CGPoint] = [
                CGPoint(x: rect.minX + 15, y: y),
                CGPoint(x: rect.maxX - 15, y: y)
            ]
            return sockets
        }
    }
    
    
    

    UIViewController Usage

    I prefer to add floating button after viewDidLoad(_ animated:). It may need to call bringSubviewToFront() if another subview blocks the floating button afterwards.

    override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            addFloatingButton()
            floatingButton?.addTarget(self, action: #selector(floatingButtonPressed), for: .touchUpInside)
        }
    
        @objc func floatingButtonPressed(){
            print("Floating button tapped")
        }
    

    UIApplication - Top View Controller

    extension UIApplication{
    
        class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
            if let navigationController = controller as? UINavigationController {
                return topViewController(controller: navigationController.visibleViewController)
            }
            if let tabController = controller as? UITabBarController {
                if let selected = tabController.selectedViewController {
                    return topViewController(controller: selected)
                }
            }
            if let presented = controller?.presentedViewController {
                return topViewController(controller: presented)
            }
            return controller
        }
    }
    

提交回复
热议问题