无论是出现模态还是用户执行任何类型的segue.
有没有办法让整个应用程序中的按钮"始终位于顶部"(不是屏幕顶部)?
有没有办法让这个按钮可以拖动并可以屏幕移动?
我正在将Apple自己的Assistive Touch作为这种按钮的一个例子.
rob*_*off 98
您可以通过创建自己的子类UIWindow,然后创建该子类的实例来完成此操作.你需要在窗口做三件事:
将其设置windowLevel为非常高的数字,例如CGFloat.max.它被钳制(从iOS 9.2开始)到10000000,但您也可以将其设置为最高值.
将窗口的背景颜色设置为nil以使其透明.
覆盖窗口的pointInside(_:withEvent:)方法,仅对按钮中的点返回true.这将使窗口仅接受按下按钮的触摸,并且所有其他触摸将被传递到其他窗口.
然后为窗口创建一个根视图控制器.创建按钮并将其添加到视图层次结构中,并告诉窗口它,以便窗口可以使用它pointInside(_:withEvent:).
还有最后一件事要做.事实证明,屏幕键盘也使用了最高的窗口级别,并且由于它可能会在您的窗口之后出现在屏幕上,它将位于窗口的顶部.您可以通过在发生这种情况时观察UIKeyboardDidShowNotification并重置窗口来解决这个问题windowLevel(因为这样做会记录下来,使您的窗口位于同一级别的所有窗口之上).
这是一个演示.我将从我将要在窗口中使用的视图控制器开始.
import UIKit
class FloatingButtonController: UIViewController {
Run Code Online (Sandbox Code Playgroud)
这是按钮实例变量.创建的人FloatingButtonController可以访问按钮以向其添加目标/操作.我稍后会证明这一点.
private(set) var button: UIButton!
Run Code Online (Sandbox Code Playgroud)
你必须"实现"这个初始化程序,但我不打算在故事板中使用这个类.
required init?(coder aDecoder: NSCoder) {
fatalError()
}
Run Code Online (Sandbox Code Playgroud)
这是真正的初始化程序.
init() {
super.init(nibName: nil, bundle: nil)
window.windowLevel = CGFloat.max
window.hidden = false
window.rootViewController = self
Run Code Online (Sandbox Code Playgroud)
设置window.hidden = false将其置于屏幕上(当前CATransaction提交时).我还需要注意键盘出现:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
}
Run Code Online (Sandbox Code Playgroud)
我需要一个对我的窗口的引用,它将是一个自定义类的实例:
private let window = FloatingButtonWindow()
Run Code Online (Sandbox Code Playgroud)
我将在代码中创建我的视图层次结构,以保持此答案自包含:
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
Run Code Online (Sandbox Code Playgroud)
没有什么特别的东西在那里.我只是创建我的根视图并在其中放置一个按钮.
为了允许用户拖动按钮,我将向按钮添加平移手势识别器:
let panner = UIPanGestureRecognizer(target: self, action: "panDidFire:")
button.addGestureRecognizer(panner)
}
Run Code Online (Sandbox Code Playgroud)
窗口将在第一次出现时显示其子视图,并在调整大小时(特别是因为界面旋转),所以我想在那些时候重新定位按钮:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
snapButtonToSocket()
}
Run Code Online (Sandbox Code Playgroud)
(稍后会详细介绍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
Run Code Online (Sandbox Code Playgroud)
你要求"捕捉",所以如果平底锅结束或取消,我会将按钮按到固定数量的位置之一,我称之为"套接字":
if panner.state == .Ended || panner.state == .Cancelled {
UIView.animateWithDuration(0.3) {
self.snapButtonToSocket()
}
}
}
Run Code Online (Sandbox Code Playgroud)
我通过重置来处理键盘通知window.windowLevel:
func keyboardDidShow(note: NSNotification) {
window.windowLevel = 0
window.windowLevel = CGFloat.max
}
Run Code Online (Sandbox Code Playgroud)
要将按钮捕捉到插座,我找到最靠近按钮位置的插座并将按钮移到那里.请注意,这不一定是您想要的界面轮换,但我会留下一个更完美的解决方案作为读者的练习.无论如何,它在旋转后将按钮保持在屏幕上.
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
}
Run Code Online (Sandbox Code Playgroud)
我在屏幕的每个角落都插了一个插座,中间有一个插座用于演示目的:
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
}
}
Run Code Online (Sandbox Code Playgroud)
最后,自定义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")
}
Run Code Online (Sandbox Code Playgroud)
正如我所提到的,我需要覆盖,pointInside(_:withEvent:)以便窗口忽略按钮外的触摸:
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)
}
}
Run Code Online (Sandbox Code Playgroud)
现在你怎么用这个东西?我下载了Apple的AdaptivePhotos示例项目,并将我的FloatingButtonController.swift文件添加到AdaptiveCode目标中.我添加了一个属性AppDelegate:
var floatingButtonController: FloatingButtonController?
Run Code Online (Sandbox Code Playgroud)
然后我添加了代码到最后application(_:didFinishLaunchingWithOptions:)创建一个FloatingButtonController:
floatingButtonController = FloatingButtonController()
floatingButtonController?.button.addTarget(self, action: "floatingButtonWasTapped", forControlEvents: .TouchUpInside)
Run Code Online (Sandbox Code Playgroud)
这些行在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)
}
Run Code Online (Sandbox Code Playgroud)
这就是我必须做的一切.但是为了演示目的,我还做了一件事:在AboutViewController中,我改为labela,UITextView所以我有办法调出键盘.
这是按钮的样子:
这是点击按钮的效果.请注意,该按钮浮动在警报上方:
这是我拿起键盘时会发生的事情:
它处理旋转吗?你打赌:
好吧,旋转处理并不完美,因为旋转后哪个插座离按钮最近可能与旋转前的逻辑"插座"不同.您可以通过跟踪按钮最后捕捉到哪个套接字并特别处理旋转(通过检测大小更改)来解决此问题.
为了您的方便,我把全部FloatingViewController.swift放在这个要点中.
Rob Mayoff 对本地浮动按钮的回答的修改版本。
UIViewController 扩展可以让你更好地控制浮动按钮。但是,它将添加到您调用addFloatingButton而不是所有视图控制器的单个视图控制器上。
对于这个问题,这可能不是最佳实践,但是这种对浮动按钮的控制应该为不同的目的启用不同的实践。
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
}
}
Run Code Online (Sandbox Code Playgroud)
我更喜欢在 viewDidLoad(_animated:) 之后添加浮动按钮。如果之后另一个子视图阻塞了浮动按钮,则可能需要调用breedSubviewToFront()。
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addFloatingButton()
floatingButton?.addTarget(self, action: #selector(floatingButtonPressed), for: .touchUpInside)
}
@objc func floatingButtonPressed(){
print("Floating button tapped")
}
Run Code Online (Sandbox Code Playgroud)
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
}
}
Run Code Online (Sandbox Code Playgroud)
小智 5
在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
}
}
Run Code Online (Sandbox Code Playgroud)
在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)
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
14183 次 |
| 最近记录: |