iOS HUD (Heads Up Display) 旋转框
一、隐藏软键盘
在 viewDidLoad() 添加 gestureRecognizer
// Hide keyboard
let gestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(hideKeyboard))
gestureRecognizer.cancelsTouchesInView = false
tableView.addGestureRecognizer(gestureRecognizer)
@objc func hideKeyboard(
_ gestureRecognizer: UIGestureRecognizer
) {
let point = gestureRecognizer.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: point)
if indexPath != nil && indexPath!.section == 0 &&
indexPath!.row == 0 {
return
}
descriptionTextView.resignFirstResponder()
}
以上代码确保只有点击 section 0, row 0 之外部分才会隐藏软键盘。
The HUD (Heads Up Display)
HUD,是 Heads-Up Display 的缩写。HUD 通常用于像下载文件或执行其它长期任务时显示进度条。
HUD 是 UIView 的子类,我们可以在其它视图之上添加 HUD。实际上,labels 是添加在 cells 顶部的 view,cells 是被添加到 table view 顶部的 view,而 table view 又是被添加在 navigation controller 的顶部的内容视图。
创建 HUD view
HudView.swift:
import UIKit
class HudView: UIView {
var text = ""
class func hud(
inView view: UIView,
animated: Bool
) -> HudView {
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
view.isUserInteractionEnabled = false
hudView.backgroundColor = UIColor(
red: 1,
green: 0,
blue: 0,
alpha: 0.5)
return hudView
}
override func draw(_ rect: CGRect) {
let boxWidth: CGFloat = 96
let boxHeight: CGFloat = 96
let boxRect = CGRect(
x: round((bounds.size.width - boxWidth) / 2),
y: round((bounds.size.height - boxHeight) / 2),
width: boxWidth,
height: boxHeight)
let roundedRect = UIBezierPath(
roundedRect: boxRect,
cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundedRect.fill()
// Draw checkmark
if let image = UIImage(named: "Checkmark") {
let imagePoint = CGPoint(
x: center.x - round(image.size.width / 2),
y: center.y - round(image.size.height / 2) - boxHeight / 8)
image.draw(at: imagePoint)
}
// Draw the text
let attribs = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
NSAttributedString.Key.foregroundColor: UIColor.white
]
let textSize = text.size(withAttributes: attribs)
let textPoint = CGPoint(
x: center.x - round(textSize.width / 2),
y: center.y - round(textSize.height / 2) + boxHeight / 4)
text.draw(at: textPoint, withAttributes: attribs)
}
}
方法 hud(inView, animated) 被称为便利构造函数。它创建并返回一个新的 HudView 实例。
使用示例:
let hudView = HudView.hud(inView: parentView, animated: true)
构造函数通常是一个类方法,即不用实例化对象就可以调用的方法,声明它,以 class func 开头,而不是 func 。
使用 HUD view
@IBAction func done() {
guard let mainView = navigationController?.parent?.view
else { return }
let hudView = HudView.hud(inView: mainView, animated: true)
hudView.text = "Tagged"
}
guard let
是一种语法糖,我们可以这样使用它:
var mainView: UIView
if let view = navigationController?.parent?.view {
mainView = view
} else {
return
}
使用 guard let 这种方式,可以精简代码。
使用导航控制器父级中的视图 - 导航控制器的父级是 tab bar controller。这样可以保证 HUD 覆盖了 navigation controller 和 tab bar controlle 的查看区域。
每当 UIKit 希望视图重绘时,都会调用 draw() 方法。
iOS 中的所有内容都是事件驱动的。除非 UIKit 要求视图自行绘制,否则视图不会在屏幕上绘制任何内容。这意味着我们永远不要自己调用 draw() 。
如果要重绘视图,则应向 UIKit 发送 setNeedsDisplay() 消息。准备好执行绘图时,UIKit 然后将触发 draw() 。
let boxRect = CGRect(
x: round((bounds.size.width - boxWidth) / 2),
y: round((bounds.size.height - boxHeight) / 2),
width: boxWidth,
height: boxHeight)
round() 确保矩形的长宽不为小数,因为这会使图像看起来模糊。
let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundedRect.fill()
用 UIBezierPath 绘制带有圆角的矩形,非常方便。
运行 app,结果如下所示:
添加动画
在 HudView.swift 添加如下代码:
// MARK: - Helper methods
func show(animated: Bool) {
if animated {
// 1
alpha = 0
transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
// 2
UIView.animate(withDuration: 0.3) {
// 3
self.alpha = 1
self.transform = CGAffineTransform.identity
}
}
}
在动画开始之前设置视图的初始状态。这里,将alpha 设置为0,使视图完全透明。将 scale factor 设置为 1.3 ,这样,视图最初会放大到比正常情况下大 1.3 倍的大小。
调用 UIView.animate(withDuration:animations:) 方法设置动画。将方法传递给闭包,在闭包中执行动画。闭包是一段内联代码,不会立即执行。UIKit 将在闭包内部设置动画终止时的状态。
在闭包内部,设置动画完成后的视图状态。将 alpha 设置为 1,让 HudView 完全不透明。我们还可以将变换设置为“identity”变换,将比例恢复为正常。由于此代码使用闭包,因此需要使用 self 来引用 HudView 实例及其属性。这就是关闭的规则。
将 hud(inView:animated:) 方法更改为在返回之前立即调用 show(animated:):
class func hud(inView view: UIView, animated: Bool) -> HudView {
. . .
hudView.show(animated: animated) // Add this
return hudView
}
优化动画
iOS 有一种称为“spring”动画的东西,它可以上下反弹,并且在视觉上比普通的旧版动画更有趣。使用起来非常简单。
修改UIView.animate(withDuration:animations:) 代码:
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: [],
animations: {
self.alpha = 1
self.transform = CGAffineTransform.identity
}, completion: nil)
Swift 5.3 引入了多个尾随闭包(这里如animations 和 completion)。但是,由于completion中没有代码,因此无法在此处使用 non-trailing closure 语法 - 除非我们将空闭包传递到 completion 闭包。
运行该 app 并观看它的反弹动画。效果更好一些!
处理 navigation
GCD 是一个非常方便但有些底层的库,可以用于处理异步任务。让 app 在执行一些代码之前等待几秒钟,是异步任务的一个完美示例。
将以下代码添加到 done() 方法底部:
let delayInSeconds = 0.6
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
self.navigationController?.popViewController(animated: true)
}
DispatchQueue.main.asyncAfter() 函数将闭包作为其最终参数。在该闭包中,我们告诉导航控制器返回到导航堆栈中的上一个视图控制器。
DispatchQueue.main.asyncAfter() 在 .now() + delayInSeconds 时间之后执行操作。
我们发现返回上一页面后,HUD 并没有消失,这是不好的体验。
我们进行如下优化,将以下方法添加到 HudView.swift:
func hide() {
superview?.isUserInteractionEnabled = true
removeFromSuperview()
}
返回上一页面之前,调用此新方法隐藏 HUD。
在 LocationDetailsViewController.swift 中修改 done() 方法:
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
hudView.hide() // Add this line
self.navigationController?.popViewController(animated: true)
}
代码精简优化
我们可以把方法抽取到一个工具类,Functions.swift:
import Foundation
func afterDelay(_ seconds: Double, run: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(
deadline: .now() + seconds,
execute: run)
}
这是一个自由函数,而不是对象内部的方法。因此,可以在代码中的任何位置使用它。
仔细看 afterDelay() 的第二个参数,一个名为 run 的参数,它的类型是 () -> Void。在 Swift 中表示带参数且无返回值的闭包。
闭包的类型通常如下所示:
(parameter list) -> return type
在这种情况下,参数列表和返回值均为空, () 和 Void。这也可以写为 Void -> Void 甚至 () -> (),但是推荐这样写 () -> Void,因为它看起来更像一个函数声明。
对于不立即执行的 closures,@escaping 注释是必需的。 这样一来,Swift 便知道应该持有该 closures 一段时间。
回到 LocationDetailsViewController.swift,按如下所示修改done() :
@IBAction func done() {
...
hudView.text = "Tagged"
afterDelay(0.6) {
hudView.hide()
self.navigationController?.popViewController(animated: true)
}
}
通过将令人讨厌的 GCD 内容移至一个新函数 afterDelay() 中,我们抽象了代码,使跟踪变得更加容易。
编写好的程序就是寻找正确的抽象(abstractions)。
注意:由于引用导航控制器 (navigation controller) 的代码位于闭包中,因此需要使用 self。在闭包内部,我们始终需要显式使用self。但是,这里并不需要在引用 hudView 的行上也加上 self,这是因为 hudView 是一个局部变量,它仅在 done() 方法内存在。