无论模态是呈现还是用户执行任何类型的segue.

有没有办法在整个应用程序中保持按钮"始终在顶部"(而不是屏幕顶部)?

有没有办法让这个按钮可以拖到屏幕上?

我把苹果自己的辅助触控作为这种按钮的一个例子.

推荐答案

您可以通过创建自己的UIWindow子类,然后创建该子类的实例来实现这一点.你需要对windows 做三件事:

  1. 将其windowLevel设置为一个非常高的数字,比如CGFloat.max.它被钳制(从iOS 9.2开始)为windowLevel00000,但您最好将其设置为可能的最高值.

  2. 将窗口的背景色设置为零以使其透明.

  3. 重写窗口的pointInside(_:withEvent:)方法,仅对按钮中的点返回true.这将使窗口只接受点击按钮的touch ,所有其他touch 将传递到其他窗口.

然后为窗口创建根视图控制器.创建按钮并将其添加到视图层次 struct 中,并告诉窗口有关它的信息,以便窗口可以在pointInside(_:withEvent:)中使用它.

还有最后一件事要做.事实证明,屏幕上的键盘也使用最高的窗口级别,因为它可能会在窗口之后出现在屏幕上,所以它将位于窗口的顶部.你可以通过观察UIKeyboardDidShowNotification并在出现这种情况时重置窗口的windowLevel来解决这个问题(因为这样做是为了将你的窗口置于同一级别的所有窗口之上).

这是一个演示.我将从窗口中使用的视图控制器开始.

import UIKit

class FloatingButtonController: UIViewController {

下面是button实例变量.任何创建FloatingButtonController的人都可以访问该按钮向其添加目标/操作.稍后我将演示这一点.

    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

设置window.hidden = false会将其显示在屏幕上(当当前CATransaction提交时).我还需要注意键盘的显示:

        NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
    }

我需要一个对我的窗口的引用,它将是一个自定义类的实例:

    private let window = FloatingButtonWindow()

我将在代码中创建我的视图层次 struct ,以保持此答案的独立性:

    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()
    }

(稍后将有更多关于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()
            }
        }
    }

我通过重置window.windowLevel来处理键盘通知:

    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
    }

}

最后,自定义UIWindow子类:

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")
    }

如前所述,我需要覆盖pointInside(_:withEvent:),以便窗口忽略按钮外的touch :

    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)
    }

}

你怎么用这个东西?我下载了Apple's AdaptivePhotos sample project并将FloatingButtonController.swift文件添加到AdaptiveCode目标.我在AppDelegate中添加了一个属性:

var floatingButtonController: FloatingButtonController?

然后我在application(_:didFinishLaunchingWithOptions:)的末尾添加了代码,创建了一个FloatingButtonController:

    floatingButtonController = FloatingButtonController()
    floatingButtonController?.button.addTarget(self, action: "floatingButtonWasTapped", forControlEvents: .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?.presentViewController(alert, animated: true, completion: nil)
}

这就是我要做的.我用了一种方式来演示:我用了一个键盘来演示.

下面是按钮的外观:

dragging the button

以下是点击按钮的效果.请注意,按钮浮动在alert 上方:

clicking the button

下面是当我打开键盘时发生的事情:

keyboard

它可以旋转吗?当然:

rotation

旋转处理并不完美,因为旋转后最靠近按钮的插座可能与旋转前的插座不同.您可以通过跟踪按钮最后一次扣在哪个插座上,并专门处理旋转(通过检测大小变化)来修复此问题.

为了方便起见,我把整个FloatingViewController.swift换成了this gist.

Swift相关问答推荐

字符串目录不适用于SWIFT包

SwiftUI map 旋转

在`NavigationStack`中使用`SafeAreaInset`修饰符时出现SwiftUI异常行为

使用MKLocalSearch获取坐标

修改数组中每个类实例的属性

如何在SwiftUI的文本视图中显示NSMutableAttributedString?

Swift 并发任务与调度队列线程:是什么决定同时运行多少任务?

Swift // Sprite Kit:类别位掩码限制

使用 Async-Await 和 Vapor-Fluent 创建 CRUD 函数 - Swift 5.6

Swiftui 按钮依次显示一组图像

如何确定扩展中的具体类型?

如何获得不同的插入和移除过渡动画?

Alamofire 会自动存储 cookie 吗?

Swift 2 或 3 中的 Google Analytics 问题

在 Swift 中将两个字节的 UInt8 数组转换为 UInt16

否定 #available 语句

在 UItextfield 的右视图上添加一个按钮,文本不应与按钮重叠

如何从 Swift 中的字符串生成条形码?

Swift UITableView reloadData 在一个闭包中

如何快速显示数组的所有元素?