在iOS中,如何创build一个始终位于所有其他视图控制器之上的button?
无论是呈现模式还是用户执行任何types的赛格。
有没有办法在整个应用程序中保持button“始终在最上方”(而不是屏幕的顶部)?
有没有办法让这个button可拖动和可以屏幕上?
作为这样的button的例子,我正在看苹果自己的辅助触摸。
你可以通过创build你自己的UIWindow
的子类,然后创build该子类的一个实例。 你需要在窗口做三件事:
-
将它的
windowLevel
设置为一个非常高的数字,比如CGFloat.max
。 它被固定(从iOS 9.2开始)到10000000,但是你可以把它设置到最高的值。 -
将窗口的背景颜色设置为零以使其透明。
-
覆盖窗口的
pointInside(_:withEvent:)
方法,仅返回button中的点的真实值。 这将使窗口只接受点击button的触摸,所有其他的触摸将传递到其他窗口。
然后为该窗口创build一个根视图控制器。 创buildbutton并将其添加到视图层次结构,并告诉窗口关于它的窗口可以在pointInside(_:withEvent:)
使用它。
还有最后一件事要做。 事实certificate,屏幕上的键盘也使用最高的窗口级别,因为它可能会在窗口后出现在屏幕上,它将在窗口顶部。 您可以通过观察UIKeyboardDidShowNotification
并在发生这种情况时重置窗口的windowLevel
来解决这个问题(因为这样做可以将窗口放在同一级别的所有窗口之上)。
这是一个演示。 我将从我要在窗口中使用的视图控制器开始。
import UIKit class FloatingButtonController: UIViewController {
这里是button实例variables。 无论谁创build一个FloatingButtonController
都可以访问该button来添加一个目标/动作。 稍后我会演示。
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()
我将在代码中创build我的视图层次结构以保持这个答案是独立的:
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
没有什么特别的。 我只是创build我的根视图,并在其中放置一个button。
要允许用户拖动button,我将为button添加一个平移手势识别器:
let panner = UIPanGestureRecognizer(target: self, action: "panDidFire:") button.addGestureRecognizer(panner) }
当窗口第一次出现时,窗口将显示它的子视图,当它被resize(特别是因为界面旋转),所以我想在这些时候重新定位button:
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() snapButtonToSocket() }
(关于snapButtonToSocket
更多信息。)
为了处理拖动button,我使用了标准方式的平移手势识别器:
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
你要求“捕捉”,所以如果平底锅正在结束或取消,我会把buttonlocking到我称为“sockets”的固定位置之一:
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 }
要将button捕捉到一个套接字,我find最接近该button位置的套接字,并在那里移动button。 请注意,这不一定是你想要的界面旋转,但我会留下一个更完美的解决scheme作为一个读者的练习。 无论如何,它会在旋转后保持屏幕上的button。
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 }
为了演示目的,我在屏幕的每个angular落放了一个sockets,中间放了一个sockets。
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:)
以便窗口忽略button外的触摸:
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的AdaptivePhotos示例项目 ,并将我的FloatingButtonController.swift
文件添加到AdaptiveCode
目标。 我添加了一个属性到AppDelegate
:
var floatingButtonController: FloatingButtonController?
然后我将代码添加到application(_:didFinishLaunchingWithOptions:)
的末尾application(_:didFinishLaunchingWithOptions:)
以创build一个FloatingButtonController
:
floatingButtonController = FloatingButtonController() floatingButtonController?.button.addTarget(self, action: "floatingButtonWasTapped", forControlEvents: .TouchUpInside)
这些行在函数结束之前return true
之前。 我还需要编写button的操作方法:
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) }
这就是我所要做的。 但是为了演示目的,我还做了一件事情:在AboutViewController中,我将label
更改为UITextView
所以我有办法调出键盘。
这是button的样子:
这是点击button的效果。 请注意,该button在警报上方浮动:
下面是我popup键盘时发生的事情:
它处理旋转? 你打赌:
那么,旋转处理是不完美的,因为旋转可能不是旋转之前相同的“逻辑”套接字,哪个套接字最接近button。 您可以通过跟踪哪个套接字button最后被捕捉到,并专门处理旋转(通过检测大小更改)来解决这个问题。
为了您的方便,我把整个FloatingViewController.swift
放在这个要点中。
苹果在这种情况下的意图是,你的根视图控制器包含button和任何你想要的触摸逻辑,它也作为一个子视图控制器embedded任何运行的应用程序的其余部分。 你不打算直接与UIWindow
交谈,如果你愿意的话,可能会遇到一些类似于iOS版本自动转换的问题。
旧的代码仍然可以使用像UIAlertView
这样的弃用类来pipe理顶层模式。 苹果公司已明确地切换到像UIAlertController
这样的方法,其中明确包含的东西拥有覆盖它的东西,以允许适当的视图控制器遏制。 您可能应该做出相同的假设,并拒绝使用基于弃用代码的组件。
你可以使用Paramount ,它基本上使用另一个UIWindow
来显示你的button
Manager.action = { print("action touched") } Manager.show()
你可以使用视图图层的zPosition
属性(它是一个CALayer
对象)来改变视图的z-index, 也需要添加这个button或视图作为你的窗口的子视图而不是任何其他视图控制器的子视图 ,它的默认值是0.但是你可以像这样改变它:
yourButton.layer.zPosition = 1;
您需要导入QuartzCore框架来访问图层。 只需在实现文件的顶部添加这行代码即可。
#import "QuartzCore/QuartzCore.h"
希望这可以帮助。
在UIWindow上添加button,它会浮在应用程序的所有视图
在此表示:
public static var topMostVC: UIViewController? { var presentedVC = UIApplication.sharedApplication().keyWindow?.rootViewController while let pVC = presentedVC?.presentedViewController { presentedVC = pVC } if presentedVC == nil { print("EZSwiftExtensions Error: You don't have any views set. You may be calling them in viewDidLoad. Try viewDidAppear instead.") } return presentedVC } topMostVC?.presentViewController(myAlertController, animated: true, completion: nil) //or let myView = UIView() topMostVC?.view?.addSubview(myView)
这些被包含在我的一个项目中的标准function: https : //github.com/goktugyil/EZSwiftExtensions
在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 } }
而在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) }
无论是呈现模式还是用户执行任何types的赛格。
简单的答案是: 是的,有一种方法。 你是程序员,所以你几乎完全控制了用户可以对你的应用程序做什么。 有些东西可能无法控制,比如当设备接到电话,把你的应用放在后台等等,但这些都是外部因素,没有人会认为是你的应用程序的一部分。
更诚实的答案是: 要做到这一点所需的努力水平可能会如此之大以至于令人望而却步。 以下是一些可能的方法:
-
您可以放弃UIKit为您提供的大部分工具,并可以自行展开模式对话框,转换等等。如果您自己编写所有内容,则无论您喜欢,都可以使其工作。 但是这是很多工作。
-
创build一个包含应用程序中所有其他视图控制器的视图控制器,自己执行转换,而不是依靠segse。 该视图控制器可以pipe理button并将其保留在视图层次结构中的所有其他视图之上。
-
创build一个通用的视图控制器超类,它pipe理一个button,然后派生所有其他视图控制器,以使您的应用程序中的每个视图控制器都有自己的button,但每个button看起来都一样,工作原理相同,用户担心只有一个button。
我认为最好的答案是: 不,在UIKit的范围内确实没有一个好方法。 你最好是努力思考自己正在努力完成的任务,并寻找方法来完成这个工作,而不是与框架一起工作。
有没有办法让这个button可拖动和可以屏幕上?
再次,你是程序员,所以你可以做你喜欢的。 在这种情况下,这个任务要简单得多。 什么是button,但触发一个行动时,触发一个视图? 即使使用标准的UIButton
类,也可以连接一个手势识别器,它可以让您拖动button,而且由于您正在编写代码,因此您可以定义button在屏幕上的显示位置,因此捕捉是非常有可能的。