在iOS中,如何创build一个始终位于所有其他视图控制器之上的button?

无论是呈现模式还是用户执行任何types的赛格。

有没有办法在整个应用程序中保持button“始终在最上方”(而不是屏幕的顶部)?

有没有办法让这个button可拖动和可以屏幕上?

作为这样的button的例子,我正在看苹果自己的辅助触摸。

你可以通过创build你自己的UIWindow的子类,然后创build该子类的一个实例。 你需要在窗口做三件事:

  1. 将它的windowLevel设置为一个非常高的数字,比如CGFloat.max 。 它被固定(从iOS 9.2开始)到10000000,但是你可以把它设置到最高的值。

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

  3. 覆盖窗口的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在屏幕上的显示位置,因此捕捉是非常有可能的。