无论模态是呈现还是用户执行任何类型的segue.
有没有办法在整个应用程序中保持按钮"始终在顶部"(而不是屏幕顶部)?
有没有办法让这个按钮可以拖到屏幕上?
我把苹果自己的辅助触控作为这种按钮的一个例子.
无论模态是呈现还是用户执行任何类型的segue.
有没有办法在整个应用程序中保持按钮"始终在顶部"(而不是屏幕顶部)?
有没有办法让这个按钮可以拖到屏幕上?
我把苹果自己的辅助触控作为这种按钮的一个例子.
您可以通过创建自己的UIWindow
子类,然后创建该子类的实例来实现这一点.你需要对windows 做三件事:
将其windowLevel
设置为一个非常高的数字,比如CGFloat.max
.它被钳制(从iOS 9.2开始)为windowLevel
00000,但您最好将其设置为可能的最高值.
将窗口的背景色设置为零以使其透明.
重写窗口的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)
}
这就是我要做的.我用了一种方式来演示:我用了一个键盘来演示.
下面是按钮的外观:
以下是点击按钮的效果.请注意,按钮浮动在alert 上方:
下面是当我打开键盘时发生的事情:
它可以旋转吗?当然:
旋转处理并不完美,因为旋转后最靠近按钮的插座可能与旋转前的插座不同.您可以通过跟踪按钮最后一次扣在哪个插座上,并专门处理旋转(通过检测大小变化)来修复此问题.
为了方便起见,我把整个FloatingViewController.swift
换成了this gist.