记录iOS开发中的一些知识点
1.常用的几个高阶函数
2.高阶函数扩展
3.优雅的判断多个值中是否包含某一个值
4.Hashable、Equatable和Comparable协议
5.可变参数函数
6.where关键字
7.switch中判断枚举类型,尽量避免使用default
8.iOS9之后全局动态修改StatusBar样式
9.使用面向协议实现app的主题功能
10.swift中多继承的实现
11.华丽的TableView刷新动效
12.实现一个不基于Runtime的KVO
13.实现多重代理
14.自动检查控制器是否被销毁
15.向控制器中注入代码
16.给Extension添加存储属性
17.用闭包实现按钮的链式点击事件
18.用闭包实现手势的链式监听事件
19.用闭包实现通知的监听事件
20.AppDelegate解耦
21.常见的编译器诊断指令
22.最后执行的defer代码块
23.定义全局常量
24.使用Codable协议解析JSON
25.dispatch_once替代方案
26.被废弃的+load()和+initialize()
27.交换方法 Method Swizzling
28.获取View的指定子视图
29.线程安全: 互斥锁和自旋锁(10种)
30.可选类型扩展
31.更明了的异常处理封装
32.关键字static和class的区别
33.在字典中用KeyPaths取值
34.给UIView顶部添加圆角
35.使用系统自带气泡弹框
36.给UILabel添加内边距
37.给UIViewController添加静态Cell
38.简化使用UserDefaults
39.给TabBar上的按钮添加动画
40.给UICollectionView的Cell添加左滑删除
41.基于NSLayoutAnchor的轻量级AutoLayout扩展
42.简化复用Cell的代码
43.正则表达式的封装
44.自定义带动画效果的模态框
45.利用取色盘获取颜色
46.第三方库的依赖隔离
47.给App的某个功能添加快捷方式
记录使用Xcode工具的一些小技巧
1.生成对外暴露的属性和方法
2.显示Storyboard中控件之间的距离
3.重命名当前文件中的方法名或变量名
4.Storyboard中视图只覆盖不被添加
5.锁定Storyboard中控件的约束
6.多重光标操作
7.断点对象预览
8.Storyboard拆分
函数式编程在swift中有着广泛的应用,下面列出了几个常用的高阶函数.
常用来对数组进行排序.顺便感受下函数式编程的多种姿势.
let intArr = [13, 45, 27, 80, 22, 53]
let sortOneArr = intArr.sorted { (a: Int, b: Int) -> Bool in
return a < b
}
// [13, 22, 27, 45, 53, 80]
let sortTwoArr = intArr.sorted { (a: Int, b: Int) in
return a < b
}
// [13, 22, 27, 45, 53, 80]
let sortThreeArr = intArr.sorted { (a, b) in
return a < b
}
// [13, 22, 27, 45, 53, 80]
let sortFourArr = intArr.sorted {
return $0 < $1
}
// [13, 22, 27, 45, 53, 80]
let sortFiveArr = intArr.sorted {
$0 < $1
}
// [13, 22, 27, 45, 53, 80]
let sortSixArr = intArr.sorted(by: <)
// [13, 22, 27, 45, 53, 80]
let mapArr = intArr.map { $0 * $0 }
// [169, 2025, 729, 6400, 484, 2809]
let optionalArr = [nil, 4, 12, 7, Optional(3), 9]
let compactMapArr = optionalArr.compactMap { $0 }
// [4, 12, 7, 3, 9]
let evenArr = intArr.filter { $0 % 2 == 0 }
// [80, 22]
// 组合成一个字符串
let stringArr = ["1", "2", "3", "*", "a"]
let allStr = stringArr.reduce("") { $0 + $1 }
// 123*a
// 求和
let sum = intArr.reduce(0) { $0 + $1 }
// 240
let chainArr = [4, 3, 5, 8, 6, 2, 4, 7]
let resultArr = chainArr.filter {
$0 % 2 == 0
}.map {
$0 * $0
}.reduce(0) {
$0 + $1
}
// 136
extension Sequence {
// 可以将一些公共功能注释为@inlinable,给编译器提供优化跨模块边界的泛型代码的选项
@inlinable
public func customMap<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] {
let initialCapacity = underestimatedCount
var result = ContiguousArray<T>()
// 因为知道当前元素个数,所以一次性为数组申请完内存,避免重复申请
result.reserveCapacity(initialCapacity)
// 获取所有元素
var iterator = self.makeIterator()
// 将元素通过参数函数处理后添加到数组中
for _ in 0..<initialCapacity {
result.append(try transform(iterator.next()!))
}
// 如果还有剩下的元素,添加进去
while let element = iterator.next() {
result.append(try transform(element))
}
return Array(result)
}
}
map的实现无非就是创建一个空数组,通过for循环遍历将每个元素通过传入的函数处理后添加到空数组中,只不过swift的实现更加高效一点.
关于其余相关高阶函数的实现:Sequence.swift
class Pet {
let type: String
let age: Int
init(type: String, age: Int) {
self.type = type
self.age = age
}
}
var pets = [
Pet(type: "dog", age: 5),
Pet(type: "cat", age: 3),
Pet(type: "sheep", age: 1),
Pet(type: "pig", age: 2),
Pet(type: "cat", age: 3),
]
pets.forEach { p in
print(p.type)
}
let cc = pets.contains { $0.type == "cat" }
let firstIndex = pets.firstIndex { $0.age == 3 }
// 1
let lastIndex = pets.lastIndex { $0.age == 3 }
// 4
let sortArr = pets.sorted { $0.age < $1.age }
let arr1 = pets.prefix { $0.age > 3 }
// [{type "dog", age 5}]
let arr2 = pets.drop { $0.age > 3 }
// [{type "cat", age 3}, {type "sheep", age 1}, {type "pig", age 2}, {type "cat", age 3}]
let line = "BLANCHE: I don't want realism. I want magic!"
let wordArr = line.split(whereSeparator: { $0 == " " })
// ["BLANCHE:", "I", "don\'t", "want", "realism.", "I", "want", "magic!"]
我们最常用的方式
let string = "One"
if string == "One" || string == "Two" || string == "Three" {
print("One")
}
这种方式是可以,但可阅读性不够,那有啥好的方式呢?
if ["One", "Two", "Three"].contains(where: { $0 == "One"}) {
print("One")
}
if string == any(of: "One", "Two", "Three") {
print("One")
}
func any<T: Equatable>(of values: T...) -> EquatableValueSequence<T> {
return EquatableValueSequence(values: values)
}
struct EquatableValueSequence<T: Equatable> {
static func ==(lhs: EquatableValueSequence<T>, rhs: T) -> Bool {
return lhs.values.contains(rhs)
}
static func ==(lhs: T, rhs: EquatableValueSequence<T>) -> Bool {
return rhs == lhs
}
fileprivate let values: [T]
}
这样做的前提是any中传入的值需要实现Equatable
协议.
实现Hashable协议的方法后我们可以根据hashValue
方法来获取该对象的哈希值.
字典中的value的存储就是根据key的hashValue
,所以所有字典中的key都要实现Hashable协议.
class Animal: Hashable {
var hashValue: Int {
return self.type.hashValue ^ self.age.hashValue
}
let type: String
let age: Int
init(type: String, age: Int) {
self.type = type
self.age = age
}
}
let a1 = Animal(type: "Cat", age: 3)
a1.hashValue
// 哈希值
实现Equatable协议后,就可以用==
符号来判断两个对象是否相等了.
class Animal: Equatable, Hashable {
static func == (lhs: Animal, rhs: Animal) -> Bool {
if lhs.type == rhs.type && lhs.age == rhs.age{
return true
}else {
return false
}
}
let type: String
let age: Int
init(type: String, age: Int) {
self.type = type
self.age = age
}
}
let a1 = Animal(type: "Cat", age: 3)
let a2 = Animal(type: "Cat", age: 4)
a1 == a2
// false
基于Equatable基础上的Comparable类型,实现相关的方法后可以使用<
、<=
、>=
、>
等符号进行比较.
class Animal: Comparable {
// 只根据年龄选项判断
static func < (lhs: Animal, rhs: Animal) -> Bool {
if lhs.age < rhs.age{
return true
}else {
return false
}
}
let type: String
let age: Int
init(type: String, age: Int) {
self.type = type
self.age = age
}
}
let a1 = Animal(type: "Cat", age: 3)
let a2 = Animal(type: "Cat", age: 4)
let a3 = Animal(type: "Cat", age: 1)
let a4 = Animal(type: "Cat", age: 6)
// 按照年龄从大到小排序
let sortedAnimals = [a1, a2, a3, a4].sorted(by: <)
在日常开发中会涉及到大量对自定义对象的比较操作,所以Comparable
协议的用途还是比较广泛的.
Comparable
协议除了应用在类上,还可以用在结构体和枚举上.
// 常用的姿势
[2, 3, 4, 5, 6, 7, 8, 9].reduce(0) { $0 + $1 }
// 44
// 使用可变参数函数
sum(values: 2, 3, 4, 5, 6, 7, 8, 9)
// 44
// 可变参数的类型是个数组
func sum(values:Int...) -> Int {
var result = 0
values.forEach({ a in
result += a
})
return result
}
应用:
// 给UIView添加子控件
let view = UIView()
let label = UILabel()
let button = UIButton()
view.add(view, label, button)
extension UIView {
/// 同时添加多个子控件
///
/// - Parameter subviews: 单个或多个子控件
func add(_ subviews: UIView...) {
subviews.forEach(addSubview)
}
}
where的主要作用是用来做限定.
// 只遍历数组中的偶数
let arr = [11, 12, 13, 14, 15, 16, 17, 18]
for num in arr where num % 2 == 0 {
// 12 14 16 18
}
enum ExceptionError:Error{
case httpCode(Int)
}
func throwError() throws {
throw ExceptionError.httpCode(500)
}
do{
try throwError()
// 通过where添加限定条件
}catch ExceptionError.httpCode(let httpCode) where httpCode >= 500{
print("server error")
}catch {
print("other error")
}
let student:(name:String, score:Int) = ("小明", 59)
switch student {
case let (_,score) where score < 60:
print("不及格")
default:
print("及格")
}
//第一种写法
func genericFunctionA<S>(str:S) where S:ExpressibleByStringLiteral{
print(str)
}
//第二种写法
func genericFunctionB<S:ExpressibleByStringLiteral>(str:S){
print(str)
}
// 为Numeric在Sequence中添加一个求和扩展方法
extension Sequence where Element: Numeric {
var sum: Element {
var result: Element = 0
for item in self {
result += item
}
return result
}
}
print([1,2,3,4].sum) // 10
let names = ["Joan", "John", "Jack"]
let firstJname = names.first(where: { (name) -> Bool in
return name.first == "J"
})
// "Joan"
let fruits = ["Banana", "Apple", "Kiwi"]
let containsBanana = fruits.contains(where: { (fruit) in
return fruit == "Banana"
})
// true
参考: Swift where 关键字
通过switch
语句来判断枚举类型,不使用default
,如果后期添加新的枚举类型,而忘记在switch
中处理,会报错,这样可以提高代码的健壮性.
enum State {
case loggedIn
case loggedOut
case startUI
}
func handle(_ state: State) {
switch state {
case .loggedIn:
showMainUI()
case .loggedOut:
showLoginUI()
// Compiler error: Switch must be exhaustive
}
}
最常用的方法是通过控制器来修改StatusBar
样式
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
注意:如果当前控制器有导航控制器,需要在导航控制器中这样设置(如下代码),不然不起作用.
override var preferredStatusBarStyle: UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}
这样做的好处是,可以针对不同的控制器设置不同的StatusBar
样式,但有时往往会多此一举,略嫌麻烦,那如何全局统一处理呢?
iOS9之前的做法比较简单,在plist
文件中设置View controller-based status bar appearance
为NO
.
在需要设置的地方添加
UIApplication.shared.setStatusBarStyle(.default, animated: true)
这样全局设置StatusBar
样式就可以了,但iOS9之后setStatusBarStyle
方法被废弃了,苹果推荐使用preferredStatusBarStyle
,也就是上面那种方法.
我们可以用UIAppearance
和导航栏的barStyle
去全局设置StatusBar
的样式.
-
UIAppearance
属性可以做到全局修改样式. -
导航栏的
barStyle
决定了NavigationBar
的外观,而barStyle
属性改变会联动到StatusBar
的样式.- 当
barStyle = .default
,表示导航栏的为默认样式,StatusBar
的样式为了和导航栏区分,就会变成黑色. - 当
barStyle = .black
,表示导航栏的颜色为深黑色,StatusBar
的样式为了和导航栏区分,就会变成白色.
这个有点绕,总之就是
StatusBar
的样式和导航栏的样式反着来. - 当
具体实现:
@IBAction func segmentedControl(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
// StatusBar为黑色,导航栏颜色为白色
UINavigationBar.appearance().barStyle = .default
UINavigationBar.appearance().barTintColor = UIColor.white
default:
// StatusBar为白色,导航栏颜色为深色
UINavigationBar.appearance().barStyle = .black
UINavigationBar.appearance().barTintColor = UIColor.darkNight
}
// 刷新window下的子控件
UIApplication.shared.windows.forEach {
$0.reload()
}
}
extension UIWindow {
func reload() {
subviews.forEach { view in
view.removeFromSuperview()
addSubview(view)
}
}
}
在修改导航栏颜色的时候,判断下导航栏颜色的深浅
extension UIColor {
func isDarkColor() -> Bool {
var w: CGFloat = 0
self.getWhite(&w, alpha: nil)
return w > 0.5 ? false : true
}
}
做为修改全局样式的UIAppearance
用起来还是很方便的,比如要修改所有UILabel
的文字颜色.
UILabel.appearance().textColor = labelColor
又或者我们只想修改某个CustomView
层级下的子控件UILabel
UILabel.appearance(whenContainedInInstancesOf: [CustomView.self]).textColor = labelColor
定义好协议中需要实现的属性和方法
protocol Theme {
// 自定义的颜色
var tint: UIColor { get }
// 定义导航栏的样式,为了联动状态栏(具体见第9小点)
var barStyle: UIBarStyle { get }
var labelColor: UIColor { get }
var labelSelectedColor: UIColor { get }
var backgroundColor: UIColor { get }
var separatorColor: UIColor { get }
var selectedColor: UIColor { get }
// 设置主题样式
func apply(for application: UIApplication)
// 对特定主题样式进行扩展
func extend()
}
对协议添加extension
,这样做的好处是,如果有多个结构体或类实现了协议,而每个结构体或类需要实现相同的方法,这些方法就可以统一放到extension
中处理,大大提高了代码的复用率.
如果结构体或类有着相同的方法实现,那么结构体或类的实现会覆盖掉协议的extension
中的实现.
extension Theme {
func apply(for application: UIApplication) {
application.keyWindow?.tintColor = tint
UITabBar.appearance().with {
$0.barTintColor = tint
$0.tintColor = labelColor
}
UITabBarItem.appearance().with {
$0.setTitleTextAttributes([.foregroundColor : labelColor], for: .normal)
$0.setTitleTextAttributes([.foregroundColor : labelSelectedColor], for: .selected)
}
UINavigationBar.appearance().with {
$0.barStyle = barStyle
$0.tintColor = tint
$0.barTintColor = tint
$0.titleTextAttributes = [.foregroundColor : labelColor]
}
UITextView.appearance().with {
$0.backgroundColor = selectedColor
$0.tintColor = tint
$0.textColor = labelColor
}
extend()
application.windows.forEach { $0.reload() }
}
// ... 其余相关UIAppearance的设置
// 如果某些属性需要在某些主题下定制,可在遵守协议的类或结构体下重写
func extend() {
// 在主题中实现相关定制功能
}
}
Demo中白色主题的UISegmentedControl
需要设置特定的颜色,我们可以在LightTheme
的extension
中重写extend()
方法.
extension LightTheme {
// 需要自定义的部分写在这边
func extend() {
UISegmentedControl.appearance().with {
$0.tintColor = UIColor.darkText
$0.setTitleTextAttributes([.foregroundColor : labelColor], for: .normal)
$0.setTitleTextAttributes([.foregroundColor : UIColor.white], for: .selected)
}
UISlider.appearance().tintColor = UIColor.darkText
}
}
在设置完UIAppearance
后需要对所有的控件进行刷新,这个操作放在apply
方法中.具体实现
extension UIWindow {
/// 刷新所有子控件
func reload() {
subviews.forEach { view in
view.removeFromSuperview()
addSubview(view)
}
}
}
swift本身并不支持多继承,但我们可以根据已有的API去实现.
swift中的类可以遵守多个协议,但是只可以继承一个类,而值类型(结构体和枚举)只能遵守单个或多个协议,不能做继承操作.
多继承的实现:协议的方法可以在该协议的extension
中实现
protocol Behavior {
func run()
}
extension Behavior {
func run() {
print("Running...")
}
}
struct Dog: Behavior {}
let myDog = Dog()
myDog.run() // Running...
无论是结构体还是类还是枚举都可以遵守多个协议,所以多继承就这么做到了.
// MARK: - 闪烁功能
protocol Blinkable {
func blink()
}
extension Blinkable where Self: UIView {
func blink() {
alpha = 1
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: [.repeat, .autoreverse],
animations: {
self.alpha = 0
})
}
}
// MARK: - 放大和缩小
protocol Scalable {
func scale()
}
extension Scalable where Self: UIView {
func scale() {
transform = .identity
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: [.repeat, .autoreverse],
animations: {
self.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
})
}
}
// MARK: - 添加圆角
protocol CornersRoundable {
func roundCorners()
}
extension CornersRoundable where Self: UIView {
func roundCorners() {
layer.cornerRadius = bounds.width * 0.1
layer.masksToBounds = true
}
}
extension UIView: Scalable, Blinkable, CornersRoundable {}
cyanView.blink()
cyanView.scale()
cyanView.roundCorners()
请看下面代码
protocol ProtocolA {
func method()
}
extension ProtocolA {
func method() {
print("Method from ProtocolA")
}
}
protocol ProtocolB {
func method()
}
extension ProtocolB {
func method() {
print("Method from ProtocolB")
}
}
class MyClass: ProtocolA, ProtocolB {}
此时ProtocolA
和ProtocolB
都有一个默认的实现方法method()
,由于编译器不知道继承过来的method()
方法是哪个,就会报错.
💎钻石问题,当某一个类或值类型在继承图谱中有多条路径时就会发生.
解决方法:
1. 在目标值类型或类中重写那个发生冲突的方法method()
.
2. 直接修改协议中重复的方法
相对来时第二种方法会好一点,所以多继承要注意,尽量避免多继承的协议中的方法的重复.
先看效果(由于这个页面的内容有点多,我尽量不放加载比较耗时的文件)
我们都知道TableView
的刷新动效是设置在tableView(_:,willDisplay:,forRowAt:)
这个方法中的.
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.alpha = 0
UIView.animate(
withDuration: 0.5,
delay: 0.05 * Double(indexPath.row),
animations: {
cell.alpha = 1
})
}
这样一个简单的淡入效果就OK了.但这样做显然不够优雅,我们如果要在多个TableView
使用这个效果该怎样封装呢?
// Animation接收三个参数
typealias Animation = (UITableViewCell, IndexPath, UITableView) -> Void
final class Animator {
private var hasAnimatedAllCells = false
private let animation: Animation
init(animation: @escaping Animation) {
self.animation = animation
}
func animate(cell: UITableViewCell, at indexPath: IndexPath, in tableView: UITableView) {
guard !hasAnimatedAllCells else {
return
}
animation(cell, indexPath, tableView)
// 确保每个cell动画只执行一次
hasAnimatedAllCells = tableView.isLastVisibleCell(at: indexPath)
}
}
enum AnimationFactory {
static func makeFade(duration: TimeInterval, delayFactor: Double) -> Animation {
return { cell, indexPath, _ in
cell.alpha = 0
UIView.animate(
withDuration: duration,
delay: delayFactor * Double(indexPath.row),
animations: {
cell.alpha = 1
})
}
}
// ...
}
将所有的动画设置封装在Animation
的闭包中.
最后我们就可以在tableView(_:,willDisplay:,forRowAt:)
这个方法中使用了
let animation = AnimationFactory.makeFade(duration: 0.5, delayFactor: 0.05)
let animator = TableViewAnimator(animation: animation)
animator.animate(cell: cell, at: indexPath, in: tableView)
动画相关的可以参考我之前写的文章 猛击
实现效果
示例Demo
Swift并没有在语言层级上支持KVO,如果要使用必须导入Foundation
框架, 被观察对象必须继承自NSObject
,这种实现方式显然不够优雅.
KVO本质上还是通过拿到属性的set方法去搞事情,基于这样的原理我们可以自己去实现.
话不多说,直接贴代码,新建一个Observable
文件
public class Observable<Type> {
// MARK: - Callback
fileprivate class Callback {
fileprivate weak var observer: AnyObject?
fileprivate let options: [ObservableOptions]
fileprivate let closure: (Type, ObservableOptions) -> Void
fileprivate init(
observer: AnyObject,
options: [ObservableOptions],
closure: @escaping (Type, ObservableOptions) -> Void) {
self.observer = observer
self.options = options
self.closure = closure
}
}
// MARK: - Properties
public var value: Type {
didSet {
removeNilObserverCallbacks()
notifyCallbacks(value: oldValue, option: .old)
notifyCallbacks(value: value, option: .new)
}
}
private func removeNilObserverCallbacks() {
callbacks = callbacks.filter { $0.observer != nil }
}
private func notifyCallbacks(value: Type, option: ObservableOptions) {
let callbacksToNotify = callbacks.filter { $0.options.contains(option) }
callbacksToNotify.forEach { $0.closure(value, option) }
}
// MARK: - Object Lifecycle
public init(_ value: Type) {
self.value = value
}
// MARK: - Managing Observers
private var callbacks: [Callback] = []
/// 添加观察者
///
/// - Parameters:
/// - observer: 观察者
/// - removeIfExists: 如果观察者存在需要移除
/// - options: 被观察者
/// - closure: 回调
public func addObserver(
_ observer: AnyObject,
removeIfExists: Bool = true,
options: [ObservableOptions] = [.new],
closure: @escaping (Type, ObservableOptions) -> Void) {
if removeIfExists {
removeObserver(observer)
}
let callback = Callback(observer: observer, options: options, closure: closure)
callbacks.append(callback)
if options.contains(.initial) {
closure(value, .initial)
}
}
public func removeObserver(_ observer: AnyObject) {
callbacks = callbacks.filter { $0.observer !== observer }
}
}
// MARK: - ObservableOptions
public struct ObservableOptions: OptionSet {
public static let initial = ObservableOptions(rawValue: 1 << 0)
public static let old = ObservableOptions(rawValue: 1 << 1)
public static let new = ObservableOptions(rawValue: 1 << 2)
public var rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
使用起来和KVO差不多.
需要监听的类
public class User {
// 监听的属性需要是Observable类型
public let name: Observable<String>
public init(name: String) {
self.name = Observable(name)
}
}
使用
// 创建对象
let user = User(name: "Made")
// 设置监听
user.name.addObserver(self, options: [.new]) { name, change in
print("name:\(name), change:\(change)")
}
// 修改对象的属性
user.name.value = "Amel" // 这时就可以被监听到
// 移除监听
user.name.removeObserver(self)
注意: 在使用过程中,如果改变value, addObserver方法不调用,很有可能是Observer对象已经被释放掉了.
作为iOS开发中最常用的设计模式之一Delegate
,只能是一对一的关系,如果要一对多,就只能使用NSNotification
了,但我们可以有更好的解决方案,多重代理.
protocol MasterOrderDelegate: class {
func toEat(_ food: String)
}
这边用了NSHashTable
来存储遵守协议的类,NSHashTable
和NSSet
类似,但又有所不同,总的来说有这几个特点:
1. NSHashTable
中的元素可以通过Hashable
协议来判断是否相等.
2. NSHashTable
中的元素如果是弱引用,对象销毁后会被移除,可以避免循环引用.
class masterOrderDelegateManager : MasterOrderDelegate {
private let multiDelegate: NSHashTable<AnyObject> = NSHashTable.weakObjects()
init(_ delegates: [MasterOrderDelegate]) {
delegates.forEach(multiDelegate.add)
}
// 协议中的方法,可以有多个
func toEat(_ food: String) {
invoke { $0.toEat(food) }
}
// 添加遵守协议的类
func add(_ delegate: MasterOrderDelegate) {
multiDelegate.add(delegate)
}
// 删除指定遵守协议的类
func remove(_ delegateToRemove: MasterOrderDelegate) {
invoke {
if $0 === delegateToRemove as AnyObject {
multiDelegate.remove($0)
}
}
}
// 删除所有遵守协议的类
func removeAll() {
multiDelegate.removeAllObjects()
}
// 遍历所有遵守协议的类
private func invoke(_ invocation: (MasterOrderDelegate) -> Void) {
for delegate in multiDelegate.allObjects.reversed() {
invocation(delegate as! MasterOrderDelegate)
}
}
}
class Master {
weak var delegate: MasterOrderDelegate?
func orderToEat() {
delegate?.toEat("meat")
}
}
class Dog {
}
extension Dog: MasterOrderDelegate {
func toEat(_ food: String) {
print("\(type(of: self)) is eating \(food)")
}
}
class Cat {
}
extension Cat: MasterOrderDelegate {
func toEat(_ food: String) {
print("\(type(of: self)) is eating \(food)")
}
}
let cat = Cat()
let dog = Dog()
let cat1 = Cat()
let master = Master()
// master的delegate是弱引用,所以不能直接赋值
let delegate = masterOrderDelegateManager([cat, dog])
// 添加遵守该协议的类
delegate.add(cat1)
// 删除遵守该协议的类
delegate.remove(dog)
master.delegate = delegate
master.orderToEat()
// 输出
// Cat is eating meat
// Cat is eating meat
- IM消息接收之后在多个地方做回调,比如显示消息,改变小红点,显示消息数.
UISearchBar
的回调,当我们需要在多个地方获取数据的时候,类似的还有UINavigationController
的回调等.
检查内存泄漏除了使用Instruments
,还有查看控制器pop
或dismiss
后是否被销毁,后者相对来说更方便一点.但老是盯着析构函数deinit
看日志输出是否有点麻烦呢?
UIViewController
有提供两个不知名的属性:
isBeingDismissed
: 当modal出来的控制器被dismiss
后的值为true
.isMovingFromParent
: 在控制器的堆栈中,如果当前控制器从父控制器中移除,值会变成true
.
如果这两个属性都为true
,表明控制器马上要被销毁了,但这是由ARC去做内存管理,我们并不知道多久之后被销毁,简单起见就设个2秒吧.
extension UIViewController {
public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
let rootParentViewController = dch_rootParentViewController
if isMovingFromParent || rootParentViewController.isBeingDismissed {
let disappearanceSource: String = isMovingFromParent ? "removed from its parent" : "dismissed"
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak self] in
if let VC = self {
assert(self == nil, "\(VC.description) not deallocated after being \(disappearanceSource)")
}
})
}
}
private var dch_rootParentViewController: UIViewController {
var root = self
while let parent = root.parent {
root = parent
}
return root
}
}
我们把这个方法添加到viewDidDisappear(_:)
中
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
dch_checkDeallocation()
}
如果发生循环引用,控制就不会被销毁,会触发assert
报错.
使用场景: 在某些控制器的viewDidLoad
方法中,我们需要添加一段代码,用于统计某个页面的打开次数.
最常用的解决方案:
在父类或者extension
中定义一个方法,然后在需要做统计的控制器的viewDidLoad
方法中调用刚刚定义好的方法.
或者还可以使用代码注入.
ViewControllerInjector.inject(into: [ViewController.self], selector: #selector(UIViewController.viewDidLoad)) {
// $0 为ViewController对象
// 统计代码...
}
swift虽然是门静态语言,但依然支持OC的runtime
.可以允许我们在静态类型中使用动态代码.代码注入就是通过runtime
的交换方法实现的.
class ViewControllerInjector {
typealias methodRef = @convention(c)(UIViewController, Selector) -> Void
static func inject(into supportedClasses: [UIViewController.Type], selector: Selector, injection: @escaping (UIViewController) -> Void) {
guard let originalMethod = class_getInstanceMethod(UIViewController.self, selector) else {
fatalError("\(selector) must be implemented")
}
var originalIMP: IMP? = nil
let swizzledViewDidLoadBlock: @convention(block) (UIViewController) -> Void = { receiver in
if let originalIMP = originalIMP {
let castedIMP = unsafeBitCast(originalIMP, to: methodRef.self)
castedIMP(receiver, selector)
}
if ViewControllerInjector.canInject(to: receiver, supportedClasses: supportedClasses) {
injection(receiver)
}
}
let swizzledIMP = imp_implementationWithBlock(unsafeBitCast(swizzledViewDidLoadBlock, to: AnyObject.self))
originalIMP = method_setImplementation(originalMethod, swizzledIMP)
}
private static func canInject(to receiver: Any, supportedClasses: [UIViewController.Type]) -> Bool {
let supportedClassesIDs = supportedClasses.map { ObjectIdentifier($0) }
let receiverType = type(of: receiver)
return supportedClassesIDs.contains(ObjectIdentifier(receiverType))
}
}
代码注入可以在不修改原有代码的基础上自定义自己所要的.相比继承,代码的可重用性会高一点,侵入性会小一点.
我们都知道Extension
中可以添加计算属性,但不能添加存储属性.
对 我们可以使用runtime
private var nameKey: Void?
extension UIView {
// 给UIView添加一个name属性
var name: String? {
get {
return objc_getAssociatedObject(self, &nameKey) as? String
}
set {
objc_setAssociatedObject(self, &nameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
给分类添加常见的数据类型属性按照上面这种方式可以实现,但如果需要给分类添加自定义对象呢?按照上面的方式会报错,错误提示我们要在自定义对象中实现copyWithZone
方法,如下代码所示
class CustomClass: NSObject, NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
private var customClassKey: Void?
extension UIView {
var customObject: CustomClass? {
get { return objc_getAssociatedObject(self, &customClassKey) as? CustomClass }
set { objc_setAssociatedObject(self, &customClassKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) }
}
}
通常按钮的点击事件我们需要这样写:
btn.addTarget(self, action: #selector(actionTouch), for: .touchUpInside)
@objc func actionTouch() {
print("按钮点击事件")
}
如果有多个点击事件,往往还要写多个方法,写多了有没有觉得有点烦,代码阅读起来还要上下跳转.
private var actionDictKey: Void?
public typealias ButtonAction = (UIButton) -> ()
extension UIButton {
// MARK: - 属性
// 用于保存所有事件对应的闭包
private var actionDict: (Dictionary<String, ButtonAction>)? {
get {
return objc_getAssociatedObject(self, &actionDictKey) as? Dictionary<String, ButtonAction>
}
set {
objc_setAssociatedObject(self, &actionDictKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
// MARK: - API
@discardableResult
public func addTouchUpInsideAction(_ action: @escaping ButtonAction) -> UIButton {
self.addButton(action: action, for: .touchUpInside)
return self
}
@discardableResult
public func addTouchUpOutsideAction(_ action: @escaping ButtonAction) -> UIButton {
self.addButton(action: action, for: .touchUpOutside)
return self
}
@discardableResult
public func addTouchDownAction(_ action: @escaping ButtonAction) -> UIButton {
self.addButton(action: action, for: .touchDown)
return self
}
// ...其余事件可以自己扩展
// MARK: - 私有方法
private func addButton(action: @escaping ButtonAction, for controlEvents: UIControl.Event) {
let eventKey = String(controlEvents.rawValue)
if var actionDict = self.actionDict {
actionDict.updateValue(action, forKey: eventKey)
self.actionDict = actionDict
}else {
self.actionDict = [eventKey: action]
}
switch controlEvents {
case .touchUpInside:
addTarget(self, action: #selector(touchUpInsideControlEvent), for: .touchUpInside)
case .touchUpOutside:
addTarget(self, action: #selector(touchUpOutsideControlEvent), for: .touchUpOutside)
case .touchDown:
addTarget(self, action: #selector(touchDownControlEvent), for: .touchDown)
default:
break
}
}
// 响应事件
@objc private func touchUpInsideControlEvent() {
executeControlEvent(.touchUpInside)
}
@objc private func touchUpOutsideControlEvent() {
executeControlEvent(.touchUpOutside)
}
@objc private func touchDownControlEvent() {
executeControlEvent(.touchDown)
}
@objc private func executeControlEvent(_ event: UIControl.Event) {
let eventKey = String(event.rawValue)
if let actionDict = self.actionDict, let action = actionDict[eventKey] {
action(self)
}
}
}
btn
.addTouchUpInsideAction { btn in
print("addTouchUpInsideAction")
}.addTouchUpOutsideAction { btn in
print("addTouchUpOutsideAction")
}.addTouchDownAction { btn in
print("addTouchDownAction")
}
利用runtime
在按钮的extension
中添加一个字典属性,key
对应的是事件类型,value
对应的是该事件类型所要执行的闭包.然后再添加按钮的监听事件,在响应方法中,根据事件类型找到并执行对应的闭包.
链式调用就是不断返回自身.
有没有觉得如果这样做代码写起来会简洁一点呢?
和tips17中的按钮点击事件类似,手势也可以封装成链式闭包回调.
view
.addTapGesture { tap in
print(tap)
}.addPinchGesture { pinch in
print(pinch)
}
public typealias GestureClosures = (UIGestureRecognizer) -> Void
private var gestureDictKey: Void?
extension UIView {
private enum GestureType: String {
case tapGesture
case pinchGesture
case rotationGesture
case swipeGesture
case panGesture
case longPressGesture
}
// MARK: - 属性
private var gestureDict: [String: GestureClosures]? {
get {
return objc_getAssociatedObject(self, &gestureDictKey) as? [String: GestureClosures]
}
set {
objc_setAssociatedObject(self, &gestureDictKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
// MARK: - API
/// 点击
@discardableResult
public func addTapGesture(_ gesture: @escaping GestureClosures) -> UIView {
addGesture(gesture: gesture, for: .tapGesture)
return self
}
/// 捏合
@discardableResult
public func addPinchGesture(_ gesture: @escaping GestureClosures) -> UIView {
addGesture(gesture: gesture, for: .pinchGesture)
return self
}
// ...省略相关手势
// MARK: - 私有方法
private func addGesture(gesture: @escaping GestureClosures, for gestureType: GestureType) {
let gestureKey = String(gestureType.rawValue)
if var gestureDict = self.gestureDict {
gestureDict.updateValue(gesture, forKey: gestureKey)
self.gestureDict = gestureDict
} else {
self.gestureDict = [gestureKey: gesture]
}
isUserInteractionEnabled = true
switch gestureType {
case .tapGesture:
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(_:)))
addGestureRecognizer(tap)
case .pinchGesture:
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinchGestureAction(_:)))
addGestureRecognizer(pinch)
default:
break
}
}
@objc private func tapGestureAction (_ tap: UITapGestureRecognizer) {
executeGestureAction(.tapGesture, gesture: tap)
}
@objc private func pinchGestureAction (_ pinch: UIPinchGestureRecognizer) {
executeGestureAction(.pinchGesture, gesture: pinch)
}
private func executeGestureAction(_ gestureType: GestureType, gesture: UIGestureRecognizer) {
let gestureKey = String(gestureType.rawValue)
if let gestureDict = self.gestureDict, let gestureReg = gestureDict[gestureKey] {
gestureReg(gesture)
}
}
}
具体实现 猛击
// 通知监听
self.observerNotification(.notifyName1) { notify in
print(notify.userInfo)
}
// 发出通知
self.postNotification(.notifyName1, userInfo: ["infoKey": "info"])
// 移除通知
self.removeNotification(.notifyName1)
public typealias NotificationClosures = (Notification) -> Void
private var notificationActionKey: Void?
// 用于存放通知名称
public enum NotificationNameType: String {
case notifyName1
case notifyName2
}
extension NSObject {
private var notificationClosuresDict: [NSNotification.Name: NotificationClosures]? {
get {
return objc_getAssociatedObject(self, ¬ificationActionKey)
as? [NSNotification.Name: NotificationClosures]
}
set {
objc_setAssociatedObject(self, ¬ificationActionKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
public func postNotification(_ name: NotificationNameType, userInfo: [AnyHashable: Any]?) {
NotificationCenter.default.post(name: NSNotification.Name(name.rawValue), object: self, userInfo: userInfo)
}
public func observerNotification(_ name: NotificationNameType, action: @escaping NotificationClosures) {
if var dict = notificationClosuresDict {
guard dict[NSNotification.Name(name.rawValue)] == nil else {
return
}
dict.updateValue(action, forKey: NSNotification.Name(name.rawValue))
self.notificationClosuresDict = dict
} else {
self.notificationClosuresDict = [NSNotification.Name(name.rawValue): action]
}
NotificationCenter.default.addObserver(self, selector: #selector(notificationAction),
name: NSNotification.Name(name.rawValue), object: nil)
}
public func removeNotification(_ name: NotificationNameType) {
NotificationCenter.default.removeObserver(self)
notificationClosuresDict?.removeValue(forKey: NSNotification.Name(name.rawValue))
}
@objc func notificationAction(notify: Notification) {
if let notificationClosures = notificationClosuresDict, let closures = notificationClosures[notify.name] {
closures(notify)
}
}
}
具体实现过程和tips17、tips18类似.
作为iOS整个项目的核心App delegate
,随着项目的逐渐变大,会变得越来越臃肿,一不小心代码就过了千行.
大型项目的App delegate
体积会大到什么程度呢?我们可以参考下国外2亿多月活的Telegram
的 App delegate.是不是吓一跳,4千多行.看到这样的代码是不是很想点击左上角的x.
是时候该给App delegate
解耦了,目标: 每个功能的配置或者初始化都分开,各自做各自的事情.App delegate
要做到只需要调用就好了.
命令模式: 发送方发送请求,然后接收方接受请求后执行,但发送方可能并不知道接受方是谁,执行的是什么操作,这样做的好处是发送方和接受方完全的松耦合,大大提高程序的灵活性.
protocol Command {
func execute()
}
struct InitializeThirdPartiesCommand: Command {
func execute() {
// 第三方库初始化代码
}
}
struct InitialViewControllerCommand: Command {
let keyWindow: UIWindow
func execute() {
// 根控制器设置代码
}
}
struct InitializeAppearanceCommand: Command {
func execute() {
// 全局外观样式配置
}
}
struct RegisterToRemoteNotificationsCommand: Command {
func execute() {
// 远程推送配置
}
}
final class StartupCommandsBuilder {
private var window: UIWindow!
func setKeyWindow(_ window: UIWindow) -> StartupCommandsBuilder {
self.window = window
return self
}
func build() -> [Command] {
return [
InitializeThirdPartiesCommand(),
InitialViewControllerCommand(keyWindow: window),
InitializeAppearanceCommand(),
RegisterToRemoteNotificationsCommand()
]
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
StartupCommandsBuilder()
.setKeyWindow(window!)
.build()
.forEach { $0.execute() }
return true
}
使用命令模式的好处是,如果要添加新的配置,设置完后只要加在StartupCommandsBuilder
中就可以了.App delegate
中不需要添加任何内容.
但这样做只能将didFinishLaunchingWithOptions
中的代码解耦,App delegate
中的其他方法怎样解耦呢?
组合模式: 可以将对象组合成树形结构来表现"整体/部分"层次结构. 组合后可以以一致的方法处理个别对象以及组合对象.
这边我们给App delegate
每个功能模块都设置一个子类,每个子类包含所有App delegate
的方法.
// 推送
class PushNotificationsAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print("推送配置")
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("推送相关代码...")
}
// 其余方法
}
// 外观样式
class AppearanceAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print("外观样式配置")
return true
}
}
// 控制器处理
class ViewControllerAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print("根控制器设置代码")
return true
}
}
// 第三方库
class ThirdPartiesConfiguratorAppDelegate: AppDelegateType {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
print("第三方库初始化代码")
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("ThirdPartiesConfiguratorAppDelegate - applicationDidEnterBackground")
}
func applicationDidBecomeActive(_ application: UIApplication) {
print("ThirdPartiesConfiguratorAppDelegate - applicationDidBecomeActive")
}
}
typealias AppDelegateType = UIResponder & UIApplicationDelegate
class CompositeAppDelegate: AppDelegateType {
private let appDelegates: [AppDelegateType]
init(appDelegates: [AppDelegateType]) {
self.appDelegates = appDelegates
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
appDelegates.forEach { _ = $0.application?(application, didFinishLaunchingWithOptions: launchOptions) }
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
appDelegates.forEach { _ = $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}
func applicationDidEnterBackground(_ application: UIApplication) {
appDelegates.forEach { _ = $0.applicationDidEnterBackground?(application)
}
}
func applicationDidBecomeActive(_ application: UIApplication) {
appDelegates.forEach { _ = $0.applicationDidBecomeActive?(application)
}
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let appDelegate = AppDelegateFactory.makeDefault()
enum AppDelegateFactory {
static func makeDefault() -> AppDelegateType {
return CompositeAppDelegate(appDelegates: [
PushNotificationsAppDelegate(),
AppearanceAppDelegate(),
ThirdPartiesConfiguratorAppDelegate(),
ViewControllerAppDelegate(),
]
)
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
_ = appDelegate.application?(application, didFinishLaunchingWithOptions: launchOptions)
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
appDelegate.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
func applicationDidBecomeActive(_ application: UIApplication) {
appDelegate.applicationDidBecomeActive?(application)
}
func applicationDidEnterBackground(_ application: UIApplication) {
appDelegate.applicationDidEnterBackground?(application)
}
}
App delegate
解耦相比命令模式,使用组合模式可自定义程度会更高一点.
swift标准库提供了很多编译器诊断指令,用于在编译阶段提前处理好相关事情.
下面列出了一些常见的编译器诊断指令:
swift4.2中添加了这两个命令,再也不用在项目中自己配置错误和警告的命令了.
警告
// Xcode会报一条黄色警告
#warning("此处逻辑有问题,明天再说")
// TODO
#warning("TODO: Update this code for the new iOS 12 APIs")
错误❌:
// 手动设置一条错误
#error("This framework requires UIKit!")
#if !canImport(UIKit)
#error("This framework requires UIKit!")
#endif
#if DEBUG
#warning("TODO: Update this code for the new iOS 12 APIs")
#endif
分别用于获取文件名,函数名称,当前所在行数,一般用于辅助日志输出.
自定义Log
public struct dc {
/// 自定义Log
///
/// - Parameters:
/// - message: 输出的内容
/// - file: 默认
/// - method: 默认
/// - line: 默认
public static func log<T>(_ message: T, file: NSString = #file, method: String = #function, line: Int = #line) {
#if DEBUG
print("\(file.pathComponents.last!):\(method)[\(line)]: \(message)")
#endif
}
}
一般用来判断当前代码块在某个版本及该版本以上是否可用.
if #available(iOS 8, *) {
// iOS 8 及其以上系统运行
}
guard #available(iOS 8, *) else {
return //iOS 8 以下系统就直接返回
}
@available(iOS 11.4, *)
func myMethod() {
// do something
}
判断是真机还是模拟器我们常用的方式是通过arch
#if (arch(i386) || arch(x86_64))
// this is the simulator
#else
// this is a real device
#endif
推荐使用targetEnvironment
来判断
#if targetEnvironment(simulator)
// this is the simulator
#else
// this is a real device
#endif
defer
这个关键字不是很常用,但有时还是很有用的.
具体用法,简而言之就是,defer
代码块会在函数的return前执行.
func printStringNumbers() {
defer { print("1") }
defer { print("2") }
defer { print("3") }
print("4")
}
printStringNumbers() // 打印 4 3 2 1
下面列举几个常见的用途:
func foo() throws {
defer {
print("two")
}
do {
print("one")
throw NSError()
print("不会执行")
}
print("不会执行")
}
do {
try foo()
} catch {
print("three")
}
// 打印 one two three
defer
可在函数throw之后被执行,而如果将代码添加到throw NSError()
底部和do{}
底部都不会被执行.
func writeFile() {
let file: FileHandle? = FileHandle(forReadingAtPath: filepath)
defer { file?.closeFile() }
// 文件相关操作
}
这样一方面可读性好一点,另一方面不会因为某个地方throw了一个错误而没有关闭资源文件了.
func getData(completion: (_ result: Result<String>) -> Void) {
var result: Result<String>?
defer {
guard let result = result else {
fatalError("We should always end with a result")
}
completion(result)
}
// result的处理逻辑
}
defer
中可以做一些result的验证逻辑,这样不会和result的处理逻辑混淆,代码清晰.
作为整个项目中通用的全局常量为了方便管理最好集中定义在一个地方.
下面介绍几种全局常量定义的姿势:
public struct Screen {
static var width: CGFloat {
return UIScreen.main.bounds.size.width
}
static var height: CGFloat {
return UIScreen.main.bounds.size.height
}
static var statusBarHeight: CGFloat {
return UIApplication.shared.statusBarFrame.height
}
}
Screen.width // 屏幕宽度
Screen.height // 屏幕高度
Screen.statusBarHeight // statusBar高度
好处是能比较直观的看出全局常量的定义逻辑,方便后面扩展.
正常情况下的enum
都是与case
搭配使用,如果使用了case
就要实例化enum
.其实也可以不写case
.
public enum ConstantsEnum {
static let width: CGFloat = 100
static let height: CGFloat = 50
}
ConstantsEnum.width
let instance = ConstantsEnum()
// ERROR: 'ConstantsEnum' cannot be constructed because it has no accessible initializers
ConstantsEnum
不可以实例化,会报错.
相比struct
,使用枚举定义常量可以避免不经意间实例化对象.
使用extension
几乎可以为任何类型扩展常量.
例如,通知名称
extension Notification.Name {
// 名称
static let customNotification = Notification.Name("customNotification")
}
NotificationCenter.default.post(name: .customNotification, object: nil)
增加自定义颜色
extension UIColor {
class var myGolden: UIColor {
return UIColor(red: 1.000, green: 0.894, blue: 0.541, alpha: 0.900)
}
}
view.backgroundColor = .myGolden
增加double常量
extension Double {
public static let kRectX = 30.0
public static let kRectY = 30.0
public static let kRectWidth = 30.0
public static let kRectHeight = 30.0
}
CGRect(x: .kRectX, y: .kRectY, width: .kRectWidth, height: .kRectHeight)
因为传入参数类型是确定的,我们可以把类型名省略,直接用点语法.
swift4.0推出的Codable
协议用来解析JSON还是挺不错的.
public protocol Decodable {
public init(from decoder: Decoder) throws
}
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public typealias Codable = Decodable & Encodable
Codable
是Decodable
和Encodable
这两个协议的综合,只要遵守了Codable
协议,编译器就能帮我们实现好一些细节,然后就可以做编码和解码操作了.
public struct Pet: Codable {
var name: String
var age: Int
}
let json = """
[{
"name": "WangCai",
"age": 2,
},{
"name": "xiaoHei",
"age": 3,
}]
""".data(using: .utf8)!
// JSON -> 模型
let decoder = JSONDecoder()
do {
// 对于数组可以使用[Pet].self
let dogs = try decoder.decode([Pet].self, from: json)
print(dogs)
}catch {
print(error)
}
// 模型 -> JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // 美化样式
do {
let data = try encoder.encode(Pet(name: "XiaoHei", age: 3))
print(String(data: data, encoding: .utf8)!)
// {
// "name" : "XiaoHei",
// "age" : 3
// }
} catch {
print(error)
}
下面我们重写系统的方法.
init(name: String, age: Int) {
self.name = name
self.age = age
}
// decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let age = try container.decode(Int.self, forKey: .age)
self.init(name: name, age: age)
}
// encoding
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
enum CodingKeys: String, CodingKey {
case name
case age
}
对于编码和解码的过程,我们都是创建一个容器,该容器有一个keyedBy
的参数,用于指定属性和JSON中key两者间的映射的规则,因此我们传CodingKeys
的类型过去,说明我们要使用该规则来映射.对于解码的过程,我们使用该容器来进行解码,指定要值的类型和获取哪一个key的值,同样的,编码的过程中,我们使用该容器来指定要编码的值和该值对应json中的key.
当然了,现实开发中需要解析的JSON不会这么简单.
let json = """
{
"aircraft": {
"identification": "NA875",
"color": "Blue/White"
},
"route": ["KTTD", "KHIO"],
"departure_time": {
"proposed": 1540868946509,
"actual": 1540869946509,
},
"flight_rules": "IFR",
"remarks": null,
"price": "NaN",
}
""".data(using: .utf8)!
public struct Aircraft: Codable {
public var identification: String
public var color: String
}
public enum FlightRules: String, Codable {
case visual = "VFR"
case instrument = "IFR"
}
public struct FlightPlan: Codable {
// 嵌套模型
public var aircraft: Aircraft
// 包含数组
public var route: [String]
// 日期处理
private var departureTime: [String: Date]
public var proposedDepartureDate: Date? {
return departureTime["proposed"]
}
public var actualDepartureDate: Date? {
return departureTime["actual"]
}
// 枚举处理
public var flightRules: FlightRules
// 空值处理
public var remarks: String?
// 特殊值处理
public var price: Float
// 下划线key转驼峰命名
private enum CodingKeys: String, CodingKey {
case aircraft
case flightRules = "flight_rules"
case route
case departureTime = "departure_time"
case remarks
case price
}
}
let decoder = JSONDecoder()
// 解码时,日期格式是13位时间戳 .base64:通过base64解码
decoder.dateDecodingStrategy = .millisecondsSince1970
// 指定 infinity、-infinity、nan 三个特殊值的表示方式
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
do {
let plan = try decoder.decode(FlightPlan.self, from: json)
plan.aircraft.color // Blue/White
plan.aircraft.identification // NA875
plan.route // ["KTTD", "KHIO"]
plan.proposedDepartureDate // 2018-10-30 03:09:06 +0000
plan.actualDepartureDate // 2018-10-30 03:25:46 +0000
plan.flightRules // instrument
plan.remarks // 可选类型 空
plan.price // nan
}catch {
print(error)
}
swift4.1中有个属性可以自动将key转化为驼峰命名:
decoder.keyDecodingStrategy = .convertFromSnakeCase
,如果CodingKeys
只是用来转成驼峰命名的话,设置好这个属性后就可以不用写CodingKeys
这个枚举了.
OC中用来保证代码块只执行一次的dispatch_once
在swfit中已经被废弃了,取而代之的是使用static let
,let
本身就带有线程安全性质的.
例如单例的实现.
final public class MySingleton {
static let shared = MySingleton()
private init() {}
}
但如果我们不想定义常量,需要某个代码块执行一次呢?
private lazy var takeOnceTime: Void = {
// 代码块...
}()
_ = takeOnceTime
定义一个懒加载的变量,防止在初始化的时候被执行.后面加一个void
,为了在_ = takeOnceTime
赋值时不耗性能,返回一个Void
类型.
lazy var
改为static let
也可以,为了使用方便,我们用一个类方法封装下
class ClassName {
private static let takeOnceTime: Void = {
// 代码块...
}()
static func takeOnceTimeFunc() {
ClassName.takeOnceTime
}
}
// 使用
ClassName.takeOnceTimeFunc()
这样就可以做到和dispatch_once
一样的效果了.
我们都知道OC中两个方法+load()
和+initialize()
.
+load()
: app启动的时候会加载所有的类,此时就会调用每个类的load方法.
+initialize()
: 第一次初始化这个类的时候会被调用.
然而在目前的swift版本中这两个方法都不可用了,那现在我们要在这个阶段搞事情该怎么做? 例如method swizzling
.
JORDAN SMITH大神给出了一种很巧解决方案.UIApplication
有一个next
属性,它会在applicationDidFinishLaunching
之前被调用,这个时候通过runtime
获取到所有类的列表,然后向所有遵循SelfAware协议的类发送消息.
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
UIApplication.runOnce
return super.next
}
}
protocol SelfAware: class {
static func awake()
}
class NothingToSeeHere {
static func harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount {
(types[index] as? SelfAware.Type)?.awake()
}
types.deallocate()
}
}
之后任何遵守SelfAware
协议实现的+awake()
方法在这个阶段都会被调用.
黑魔法Method Swizzling
在swift中实现的两个困难点
- swizzling 应该保证只会执行一次.
- swizzling 应该在加载所有类的时候调用.
分别在tips25
和tips26
中给出了解决方案.
下面给出了两个示例供参考:
protocol SelfAware: class {
static func awake()
static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector)
}
extension SelfAware {
static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(forClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
guard (originalMethod != nil && swizzledMethod != nil) else {
return
}
if class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
} else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
}
class NothingToSeeHere {
static func harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount {
(types[index] as? SelfAware.Type)?.awake()
}
types.deallocate()
}
}
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
UIApplication.runOnce
return super.next
}
}
在SelfAware
的extension
中为swizzlingForClass
做了默认实现,相当于一层封装.
extension UIButton: SelfAware {
static func awake() {
UIButton.takeOnceTime
}
private static let takeOnceTime: Void = {
let originalSelector = #selector(sendAction)
let swizzledSelector = #selector(xxx_sendAction(action:to:forEvent:))
swizzlingForClass(UIButton.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector)
}()
@objc public func xxx_sendAction(action: Selector, to: AnyObject!, forEvent: UIEvent!) {
struct xxx_buttonTapCounter {
static var count: Int = 0
}
xxx_buttonTapCounter.count += 1
print(xxx_buttonTapCounter.count)
xxx_sendAction(action: action, to: to, forEvent: forEvent)
}
}
extension UIViewController: SelfAware {
static func awake() {
swizzleMethod
}
private static let swizzleMethod: Void = {
let originalSelector = #selector(viewWillAppear(_:))
let swizzledSelector = #selector(swizzled_viewWillAppear(_:))
swizzlingForClass(UIViewController.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector)
}()
@objc func swizzled_viewWillAppear(_ animated: Bool) {
swizzled_viewWillAppear(animated)
print("swizzled_viewWillAppear")
}
}
通过递归获取指定view
的所有子视图.
使用
let subViewArr = view.getAllSubViews() // 获取所有子视图
let imageViewArr = view.getSubView(name: "UIImageView") // 获取指定类名的子视图
实现
extension UIView {
private static var getAllsubviews: [UIView] = []
public func getSubView(name: String) -> [UIView] {
let viewArr = viewArray(root: self)
UIView.getAllsubviews = []
return viewArr.filter {$0.className == name}
}
public func getAllSubViews() -> [UIView] {
UIView.getAllsubviews = []
return viewArray(root: self)
}
private func viewArray(root: UIView) -> [UIView] {
for view in root.subviews {
if view.isKind(of: UIView.self) {
UIView.getAllsubviews.append(view)
}
_ = viewArray(root: view)
}
return UIView.getAllsubviews
}
}
extension NSObject {
var className: String {
let name = type(of: self).description()
if name.contains(".") {
return name.components(separatedBy: ".")[1]
} else {
return name
}
}
}
UIAlertController
好用,但可自定义程度不高,例如我们想让message
文字左对齐,就需要获取到messageLabel
,但UIAlertController
并没有提供这个属性.
我们就可以通过递归拿到alertTitleLabel
和alertMessageLabel
.
extension UIAlertController {
public var alertTitleLabel: UILabel? {
return self.view.getSubView(name: "UILabel").first as? UILabel
}
public var alertMessageLabel: UILabel? {
return self.view.getSubView(name: "UILabel").last as? UILabel
}
}
虽然通过这种方法可以拿到alertTitleLabel
和alertMessageLabel
.但没法区分哪个是哪个,alertTitleLabel
为默认子控件的第一个label
,如果title
传空,message
传值,alertTitleLabel
和alertMessageLabel
获取到的都是message
的label
.
如果有更好的方法欢迎讨论.
无并发,不编程.提到多线程就很难绕开锁🔐.
iOS开发中较常见的两类锁:
自旋锁较适用于锁的持有者保存时间较短的情况下,实际使用中互斥锁会用的多一些.
四种锁分别是:
NSLock
、NSConditionLock
、NSRecursiveLock
、NSCondition
NSLocking
协议
public protocol NSLocking {
public func lock()
public func unlock()
}
下面举个多个售票点同时卖票的例子
var ticket = 20
var lock = NSLock()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let thread1 = Thread(target: self, selector: #selector(saleTickets), object: nil)
thread1.name = "售票点A"
thread1.start()
let thread2 = Thread(target: self, selector: #selector(saleTickets), object: nil)
thread2.name = "售票点B"
thread2.start()
}
@objc private func saleTickets() {
while true {
lock.lock()
Thread.sleep(forTimeInterval: 0.5) // 模拟延迟
if ticket > 0 {
ticket = ticket - 1
print("\(String(describing: Thread.current.name!)) 卖出了一张票,当前还剩\(ticket)张票")
lock.unlock()
}else {
print("oh 票已经卖完了")
lock.unlock()
break;
}
}
}
遵守协议后实现的两个方法lock()
和unlock()
,意如其名.
除此之外NSLock
、NSConditionLock
、NSRecursiveLock
、NSCondition
四种互斥锁各有其实现:
// 尝试去锁,如果成功,返回true,否则返回false
open func `try`() -> Bool
// 在limit时间之前获得锁,没有返回NO
open func lock(before limit: Date) -> Bool
// 当前线程挂起
open func wait()
// 当前线程挂起,设置一个唤醒时间
open func wait(until limit: Date) -> Bool
// 唤醒在等待的线程
open func signal()
// 唤醒所有NSCondition挂起的线程
open func broadcast()
当调用wait()
之后,NSCondition
实例会解锁已有锁的当前线程,然后再使线程休眠,当被signal()
通知后,线程被唤醒,然后再给当前线程加锁,所以看起来好像wait()
一直持有该锁,但根据苹果文档中说明,直接把wait()
当线程锁并不能保证线程安全.
NSConditionLock
是借助NSCondition
来实现的,在NSCondition
的基础上加了限定条件,可自定义程度相对NSCondition
会高些.
// 锁的时候还需要满足condition
open func lock(whenCondition condition: Int)
// 同try,同样需要满足condition
open func tryLock(whenCondition condition: Int) -> Bool
// 同unlock,需要满足condition
open func unlock(withCondition condition: Int)
// 同lock,需要满足condition和在limit时间之前
open func lock(whenCondition condition: Int, before limit: Date) -> Bool
定义了可以多次给相同线程上锁并不会造成死锁的锁.
提供的几个方法和NSLock
类似.
DispatchSemaphore
中的信号量,可以解决资源抢占的问题,支持信号的通知和等待.每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,如果信号量为0则信号会处于等待状态.直到信号量大于0开始执行.所以我们一般将DispatchSemaphore
的value设置为1.
下面给出了DispatchSemaphore
的封装类
class GCDSemaphore {
// MARK: 变量
fileprivate var dispatchSemaphore: DispatchSemaphore!
// MARK: 初始化
public init() {
dispatchSemaphore = DispatchSemaphore(value: 0)
}
public init(withValue: Int) {
dispatchSemaphore = DispatchSemaphore(value: withValue)
}
// 执行
public func signal() -> Bool {
return dispatchSemaphore.signal() != 0
}
public func wait() {
_ = dispatchSemaphore.wait(timeout: DispatchTime.distantFuture)
}
public func wait(timeoutNanoseconds: DispatchTimeInterval) -> Bool {
if dispatchSemaphore.wait(timeout: DispatchTime.now() + timeoutNanoseconds) == DispatchTimeoutResult.success {
return true
} else {
return false
}
}
}
栅栏函数也可以做线程同步,当然了这个肯定是要并行队列中才能起作用.只有当当前的并行队列执行完毕,才会执行栅栏队列.
/// 创建并发队列
let queue = DispatchQueue(label: "queuename", attributes: .concurrent)
/// 异步函数
queue.async {
for _ in 1...5 {
print(Thread.current)
}
}
queue.async {
for _ in 1...5 {
print(Thread.current)
}
}
/// 栅栏函数
queue.async(flags: .barrier) {
print("barrier")
}
queue.async {
for _ in 1...5 {
print(Thread.current)
}
}
pthread
表示POSIX thread
,跨平台的线程相关的API,pthread_mutex
也是一种互斥锁,互斥锁的实现原理与信号量非常相似,阻塞线程并睡眠,需要进行上下文切换.
一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃.假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁.
这边给出了一个基于pthread_mutex_t
(安全的"FIFO"互斥锁)的封装 MutexLock
日常开发中最常用的应该是@synchronized,这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁.这样,可以保证变量在作用范围内不会被其他线程改变.但是在swift中它已经不存在了.其实@synchronized在幕后做的事情是调用了objc_sync
中的objc_sync_enter
和objc_sync_exit
方法,并且加入了一些异常判断.
因此我们可以利用闭包自己封装一套.
func synchronized(lock: AnyObject, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
// 使用
synchronized(lock: AnyObject) {
// 此处AnyObject不会被其他线程改变
}
OSSpinLock
是执行效率最高的锁,不过在iOS10.0以后已经被废弃了.
详见大神ibireme的不再安全的 OSSpinLock
它能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.这是苹果为了取代OSSPinLock
新出的一个能够避免优先级带来的死锁问题的一个锁,OSSPinLock
就是有由于优先级造成死锁的问题.
注意: 这个锁适用于小场景下的一个高效锁,否则会大量消耗cpu资源.
var unsafeMutex = os_unfair_lock()
os_unfair_lock_lock(&unsafeMutex)
os_unfair_lock_trylock(&unsafeMutex)
os_unfair_lock_unlock(&unsafeMutex)
这边给出了基于os_unfair_lock
的封装 MutexLock
这边贴一张大神ibireme在iPhone6、iOS9对各种锁的性能测试图
参考:
不再安全的OSSpinLock
深入理解iOS开发中的锁
Optional
(可选类型)为swift的类型安全起到了巨大的作用。
几种将可选值解包的操作。
var optionalStr: String? = "可选类型"
// 强制解包
print(optionalStr!)
// (Optional binding)可选绑定解包
if let optionalStr = optionalStr {
print(optionalStr)
}
// guard解包
guard let optionalStr2 = optionalStr else {
return
}
print(optionalStr2)
// ?? 如果??前面的值为空,就输出后面的值
print(optionalStr ?? "optionalStr为空")
这是常见的几种解包方式
- 强制解包不太推荐使用,除非真的很确定当前可选类型不为空
- 可选绑定解包,虽然可以保证安全,但使用多了很容易造成层层嵌套,阅读性不好
guard
解包虽然能避免层层嵌套,但如果return
下面还有需要执行的业务逻辑咋办??
用起来很方便,但后面只能是值,或者表达式,可能满足不了要求
其实我们可以用extension
为Optional
添加自定义的API。
extension Optional {
/// 判断是否为空
var isNone: Bool {
switch self {
case .none:
return true
case .some:
return false
}
}
/// 判断是否有值
var isSome: Bool {
return !isNone
}
}
optionalStr.isNone
这样使用比if optionalStr == nil
简洁一些。
extension Optional {
/// 返回解包后的值或者默认值
func or(_ default: Wrapped) -> Wrapped {
return self ?? `default`
}
/// 返回解包后的值或`else`表达式的值
func or(else: @autoclosure () -> Wrapped) -> Wrapped {
return self ?? `else`()
}
/// 返回解包后的值或执行闭包返回值
func or(else: () -> Wrapped) -> Wrapped {
return self ?? `else`()
}
}
@autoclosure
关键词可以让表达式自动封装成一个闭包。从而可以去掉{}
.or
为??
做了一层封装,当可选值为空时,执行??后面的表达式,或者闭包。
// 为??做了一层封装
print(optionalStr.or("为空"))
// 之前的写法
if viewController == nil {
viewController = UIViewController()
}
// 使用or的写法
var viewController: UIViewController?
viewController = viewController.or(else: UIViewController())
// or的else参数传入闭包
var firstView: UIView? = nil
firstView = firstView.or { () -> UIView in
let view = UIView()
// ...其他属性设置
return view
}
extension Optional {
/// 当可选值不为空时,执行 `some` 闭包
func on(some: () throws -> Void) rethrows {
if self != nil { try some() }
}
/// 当可选值为空时,执行 `none` 闭包
func on(none: () throws -> Void) rethrows {
if self == nil { try none() }
}
}
可选值为空和不为空执行的两个闭包。
let firstView: UIView? = nil
firstView.on(some: {
print("不为nil执行的闭包")
})
firstView.on(none: {
print("为nil执行的闭包")
})
extension Optional {
/// 返回解包后的`map`过的值,如果为空,则返回默认值
func map<T>(_ closure: (Wrapped) throws -> T, default: T) rethrows -> T {
return try map(closure) ?? `default`
}
/// 返回解包后的`map`过的值,如果为空,则调用else闭包
func map<T>(_ closure: (Wrapped) throws -> T, else: () throws -> T) rethrows -> T {
return try map(closure) ?? `else`()
}
/// 可选值不为空时执行then闭包,返回执行结果
/// 可链式调用
func and<T>(then: (Wrapped) throws -> T?) rethrows -> T? {
guard let unwrapped = self else { return nil }
return try then(unwrapped)
}
/// 可选值不为空且可选值满足 `predicate` 条件才返回,否则返回 `nil`
func filter(_ predicate: (Wrapped) -> Bool) -> Wrapped? {
guard let unwrapped = self,
predicate(unwrapped) else { return nil }
return self
}
}
let optionalInt: Int? = nil
// 使用前
print(optionalArr.map({$0 * $0 }) ?? 3)
// 使用后,这样可阅读性会更好一些
print(optionalArr.map({ $0 * $0 }, default: 3))
// else后添加闭包
print(optionalArr.map({ $0 * $0 }, else: { return 3 }))
// 使用链式调用去空格并转大写
let optionalString: String? = "Hello World"
print(optionalString.and(then: {$0.filter{$0 != " "}}).and(then:{$0.uppercased()}).or("为空")) // 打印 HELLOWORLD
具体代码 猛击
参考:
Useful Optional Extensions
// 错误类型
enum ExceptionError: Error {
case httpCode(Int)
}
// 可能会抛出异常的方法
func throwError(code: Int) throws -> Int {
if code == 200 {
return code
} else {
throw ExceptionError.httpCode(code)
}
}
do {
let result = try throwError(code: 300) // 返回值
} catch {
print(error)
}
当do
代码块捕捉到异常时放在catch
中处理。
let error = should {
let result = try throwError(code: 300) // 返回值
}
func should(_ try: () throws -> Void) -> Error? {
do {
try `try`()
return nil
} catch let error {
return error
}
}
在很多情况下,这样的处理方式更方便一些。
在class
中static
和class
关键字都可以来修饰属性和方法,但它们有着本质的不同。
static
关键字:它能够用在所有类型(class
、struct
、enum
),表示静态方法或静态属性(计算属性和存储属性)。
class
关键字:只能够用在class
中,表示类方法或类属性(只能是计算属性)。
计算属性: 不直接存储值,而是提供一个
getter
和setter
方法来获取和设置其他属性或变量的值。
存储属性: 就是定义一个常量或者变量来存储值。
class MyClass {
class var name: String {
return "className"
}
static var staticName: String {
return "staticName"
}
class func classDescription() {
print("classDescription")
}
static func staticDescription() {
print("staticDescription")
}
}
class MyClassChild: MyClass {
override class var name: String {
return "className"
}
// override static var staticName: String {
// return "staticName"
// }
// Error: Cannot override static var
override class func classDescription() {
print("classDescription")
}
// override static func staticDescription() {
// print("staticDescription")
// }
// Error: Cannot override static method
}
print(MyClass.name) // 打印:className
print(MyClass.staticName) // 打印:staticName
MyClass.classDescription() // 打印:classDescription
MyClass.staticDescription() // 打印:staticDescription
print(MyClassChild.name) // 打印:className
MyClassChild.classDescription() // 打印:classDescription
使用static
修饰的类方法和类属性无法在子类中重写,相当于final class
。
作为一门强类型语言,swift
对于层层嵌套的Dictionary
类型的取值一点也不友好。
比如我们要获取下面这个字典中city
对应的值
var dict: [String: Any] = [
"msg": "success",
"code": "200",
"data": [
"userInfo": [
"name": "Dariel",
"city": "HangZhou",
],
"other": [
"sign": "9527",
]
]
]
let city = ((dict["data"] as? [String: Any])?["userInfo"] as? [String: Any])?["city"] ?? "为空" // HangZhou
这种方式跟OC的NSDictionary
取值比起来是又臭又长,不推荐。
在取值的过程中,既然每次都要将[String: Any]
类型中取出来的值,转化为String: Any]
,那为何不干脆写个分类自动转。
extension Dictionary {
subscript(dictForKey key: Key) -> [String: Any]? {
get { return self[key] as? [String: Any] }
set { self[key] = newValue as? Value }
}
// 最后一次取值返回字符串
subscript(stringForKey key: Key) -> String? {
get { return self[key] as? String }
set { self[key] = newValue as? Value }
}
// 最后一次取值返回类型可自己扩展
// ...
}
let city = dict[dictForKey: "data"]?[dictForKey: "userInfo"]?[stringForKey: "city"] ?? "" // HangZhou
这样看起来就好多了,把类型转换交给extension
去做。
如果自定义程度高一点,是不是还会有更方便的取值方式呢?我们可以参照下KVC
的。
class Person: NSObject {
@objc dynamic var firstName: String = ""
init(firstName: String) {
self.firstName = firstName
}
}
let john = Person(firstName: "John")
// #keyPath()编译的时候检查表达式是否有效
john.setValue("Dariel", forKey: #keyPath(Person.firstName))
john.value(forKeyPath: #keyPath(Person.firstName)) // 打印 Dariel
通过keyPath
取值,以这样的方式
let city = dict[keyPath: "data.userInfo.city"] ?? "" // HangZhou
具体实现:
extension Dictionary where Key: StringProtocol {
subscript(keyPath keyPath: KeyPath) -> Any? {
get {
switch keyPath.headAndTail() {
case nil:
return nil
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
return self[Key(string: head)]
case let (head, remainingKeyPath)?:
let key = Key(string: head)
switch self[key] {
case let nestedDict as [Key: Any]:
return nestedDict[keyPath: remainingKeyPath]
default:
return nil
}
}
}
set {
switch keyPath.headAndTail() {
case nil:
return
case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty:
let key = Key(string: head)
self[key] = newValue as? Value
case let (head, remainingKeyPath)?:
let key = Key(string: head)
let value = self[key]
switch value {
case var nestedDict as [Key: Any]:
nestedDict[keyPath: remainingKeyPath] = newValue
self[key] = nestedDict as? Value
default:
return
}
}
}
}
}
struct KeyPath {
var segments: [String]
var isEmpty: Bool { return segments.isEmpty }
var path: String {
return segments.joined(separator: ".")
}
// 获取当前.前面的头部和后面的部分
func headAndTail() -> (head: String, tail: KeyPath)? {
guard !isEmpty else { return nil }
var tail = segments
let head = tail.removeFirst()
return (head, KeyPath(segments: tail))
}
}
extension KeyPath {
init(_ string: String) {
segments = string.components(separatedBy: ".")
}
}
// 为了可以以这样的方式 let path: KeyPath = "123" 创建对象
extension KeyPath: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(value)
}
init(unicodeScalarLiteral value: String) {
self.init(value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
// 这个协议的作用: 保证Dictionary中的key是个字符串
protocol StringProtocol {
init(string s: String)
}
extension String: StringProtocol {
init(string s: String) {
self = s
}
}
之前给UIView
添加圆角,都是通过分类去操作。
extension UIView {
/// 设置顶部两个圆角
///
/// - Parameter radius: 圆角半径
public func topRoundCorners(radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: UIRectCorner(rawValue: UIRectCorner.topLeft.rawValue | UIRectCorner.topRight.rawValue) , cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
ios11出了一个属性maskedCorners
,共有四种类型:
- layerMinXMinYCorner 左上角
- layerMaxXMinYCorner 右上角
- layerMinXMaxYCorner 左下角
- layerMaxXMaxYCorner 右下角
if #available(iOS 11, *) {
darkView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
darkView.layer.cornerRadius = 8
}
iOS中提供这几种转场样式
- Show: 用在
UINavigationController
堆栈视图时,presentedViewController
进入时由右向左,退出时由左向右。新压入的视图控制器有返回按钮,单击可以返回。 - Show Detail: 只适用于嵌入在
UISplitViewController
对象内的视图控制器,分割控制器用以替换详细控制器,不提供返回按钮。 - Present Modally: 有多种不同呈现方式,可根据需要设置。在
iPhone
中,一般以动画的形式自下向上覆盖整个屏幕。 - Present As Popover: 在
iPad
中,目标视图以浮动窗样式呈现,点击目标视图以外区域,目标视图消失;在iPhone
中,默认目标视图以模态覆盖整个屏幕。 - Custom: 可自定义转场样式。
我们平时用的比较多的是Show
和Present Modally
,Present As Popover
这种气泡弹出样式是用在iPad
上的,但有时iPhone
上会用到,我们可以做下特殊处理,不让它覆盖整个屏幕。
实现
- 设置
PopoverView
控制器的尺寸。 - 添加
segue
并进行绑定。
具体步骤参考:How to popover not full screen
extension ViewController: UIPopoverPresentationControllerDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "popoverSegue" {
let popoverViewController = segue.destination
popoverViewController.popoverPresentationController!.delegate = self
}
}
// 特殊处理 返回none 不让PopoverView覆盖整个屏幕
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
setAlphaOfBackgroundViews(alpha: 1)
}
func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
setAlphaOfBackgroundViews(alpha: 0.8)
}
// 设置灰色背景
func setAlphaOfBackgroundViews(alpha: CGFloat) {
let statusBarWindow = UIApplication.shared.value(forKey: "statusBarWindow") as? UIWindow
UIView.animate(withDuration: 0.2) {
statusBarWindow?.alpha = alpha
self.view.alpha = alpha
self.navigationController?.navigationBar.alpha = alpha
}
}
}
因为PopoverView
是一个控制器,相比第三方气泡弹框,可自定义程度会高一点。
之前给Label
设置左右内边距都是文字加空格,但觉得这样的方式不优雅。要是碰到需要设置上下内边距该咋办?
CSS
中用padding
设置内边距,给了我们一个解决办法的思路。
实现过程
@IBDesignable
class EdgeInsetLabel: UILabel {
var textInsets = UIEdgeInsets.zero {
didSet { invalidateIntrinsicContentSize() }
}
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let insetRect = bounds.inset(by: textInsets)
let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines)
let invertedInsets = UIEdgeInsets(top: -textInsets.top,
left: -textInsets.left,
bottom: -textInsets.bottom,
right: -textInsets.right)
return textRect.inset(by: invertedInsets)
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: textInsets))
}
}
extension EdgeInsetLabel {
@IBInspectable
var leftTextInset: CGFloat {
set { textInsets.left = newValue }
get { return textInsets.left }
}
@IBInspectable
var rightTextInset: CGFloat {
set { textInsets.right = newValue }
get { return textInsets.right }
}
@IBInspectable
var topTextInset: CGFloat {
set { textInsets.top = newValue }
get { return textInsets.top }
}
@IBInspectable
var bottomTextInset: CGFloat {
set { textInsets.bottom = newValue }
get { return textInsets.bottom }
}
}
设置上下左右的内边距分别为: 10 20 30 40
@IBDesignable
和@IBInspectable
可以在使用StoryBoard
和Xib
时有更好的体验。
@IBDesignable
修饰的类可以变得所见即所得,我们可以把cornerRadius
、borderWidth
、borderColor
、shadowRadius
、shadowOpacity
、shadowOffset
、shadowColor
都交给它去做。
在StoryBoard
和Xib
可以达到如下图效果。
具体实现 猛击
正常情况下,我们可以给UIViewController
添加UITableView
,但如果添加完之后想把Content
设置为Static Cells
时会报错。
error: Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances
只有在UITableViewController
才能设置静态Cell。
我们可以采取一个折中的办法,在UIViewController
中添加一个UITableViewController
子控制器。
在StoryBoard
中的操作步骤:
1.添加Container View
到UIViewController
,设置好相关尺寸。
2.删除右边的UIViewController
,再添加一个UITableViewController
,拖线的时候注意是Embed
然后用代理在UIViewController
中操作UITableViewController
。
用来做简单数据存储的Preference
在我们的日常开发中使用的还是比较多的,但使用起来总感觉不那么方便。比如说需要去手动管理key
,之前是这样做的。
public enum UserDefaultsKey: String {
case keyOne
case keyTwo
}
extension UserDefaults {
/// 存储
public final class func set(_ value: Any, forKey: UserDefaultsKey) {
UserDefaults.standard.set(value, forKey: forKey.rawValue)
}
/// 读取
public final class func getString(forKey: UserDefaultsKey) -> String? {
return UserDefaults.standard.string(forKey: forKey.rawValue)
}
public final class func getBool(forKey: UserDefaultsKey) -> Bool? {
return UserDefaults.standard.bool(forKey: forKey.rawValue)
}
}
// 存储数据
UserDefaults.set(true, forKey: .keyOne)
// 读取数据
UserDefaults.getBool(forKey: .keyOne)
我们可以通过使用#function
避免手动管理key
,在存储和读取数据时调动的set
和get
方法也可以交给目标属性默认的set
和get
方法去做,。
extension UserDefaults {
/// 通过下标使用枚举
subscript<T: RawRepresentable>(key: String) -> T? {
get {
if let rawValue = value(forKey: key) as? T.RawValue {
return T(rawValue: rawValue)
}
return nil
}
set { set(newValue?.rawValue, forKey: key) }
}
subscript<T>(key: String) -> T? {
get { return value(forKey: key) as? T }
set { set(newValue, forKey: key) }
}
}
struct Preference {
/// bool
static var isFirstLogin: Bool {
get { return UserDefaults.standard[#function] ?? false }
set { UserDefaults.standard[#function] = newValue }
}
/// enum
static var appTheme: Theme {
get { return UserDefaults.standard[#function] ?? .light }
set { UserDefaults.standard[#function] = newValue }
}
/// 测试服跟正式服之间的切换(默认正式服)
static var serverUrl: ServerUrlType {
get { return UserDefaults.standard[#function] ?? .distributeServer }
set { UserDefaults.standard[#function] = newValue }
}
}
enum Theme: Int {
case light
case dark
case blue
}
enum ServerUrlType: String {
case developServer = "url: developServer" // 测试服
case distributeServer = "url: distributeServer" // 正式服
}
// 存储数据
Preference.isFirstLogin = true
Preference.appTheme = .dark
Preference.serverUrl = .developServer
// 读取数据
Preference.isFirstLogin // true
Preference.appTheme == .dark // true
Preference.serverUrl.rawValue // url: developServer
在测试环节经常需要在测试服和正式服来回切换,为了避免老是打包,我们可以利用UserDefaults
去更改服务器地址,在适当的位置(可以是个测试页面)加个UISwitch
,然后设置serverUrl
的值。
UserDefaults
有性能问题吗?
UserDefaults
是带缓存的。它会把访问到的key
缓存到内存中,下次再访问时,如果内存中命中就直接访问,如果未命中再从文件中载入。它还会时不时调用同步方法来保证内存与文件中的数据的一致性,有时在写入一个值后也最好调用下这个方法来保证数据真正写入文件。
UITabBarItem
中无法直接获取到按钮的UIImageView
和UILabel
,我们可以参照tips28
,根据类名获取指定子视图。
TabBar
上的按钮动画加在didSelect
方法中。
extension TabBarController {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let idx = tabBar.items?.index(of: item),
// tips28获取相关子视图
let imageView = tabBar.getSubView(name: "UITabBarSwappableImageView")[idx] as? UIImageView,
let label = tabBar.getSubView(name: "UITabBarButtonLabel")[idx] as? UILabel else {
return
}
let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
bounceAnimation.values = [1.0, 1.4, 0.9, 1.02, 1.0]
bounceAnimation.duration = TimeInterval(0.3)
bounceAnimation.calculationMode = CAAnimationCalculationMode.cubic
imageView.layer.add(bounceAnimation, forKey: nil)
label.layer.add(bounceAnimation, forKey: nil)
}
}
我们都知道UITableView
有现成的API
可以用来添加左滑删除功能,但如果想在UICollectionView
中添加左滑删除功能就只能自定义了。
其实自定义的思路也是蛮简单的,在Cell
上添加一个可以左右滚动的UIScrollView
,把删除按钮放到右边,再用代理传递删除事件。
这边使用iOS9时出的NSLayoutAnchor
写布局,相比NSLayoutConstraint
,代码简化了很多,可读性也好了很多。
下面给出了UICollectionViewCell
的基类EditingCollectionViewCell
的实现过程。
protocol EditableCollectionViewCellDelegate: class {
func hiddenContainerViewTapped(inCell cell: UICollectionViewCell)
}
class EditingCollectionViewCell: UICollectionViewCell {
// MARK: Properties
private let scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
let visibleContainerView = UIView()
let hiddenContainerView = UIView()
weak var delegate: EditableCollectionViewCellDelegate?
// MARK: Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupGestureRecognizer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.addArrangedSubview(visibleContainerView)
stackView.addArrangedSubview(hiddenContainerView)
addSubview(scrollView)
scrollView.pinEdgesToSuperView()
scrollView.addSubview(stackView)
stackView.pinEdgesToSuperView()
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 2).isActive = true
}
private func setupGestureRecognizer() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hiddenContainerViewTapped))
hiddenContainerView.addGestureRecognizer(tapGestureRecognizer)
}
@objc private func hiddenContainerViewTapped() {
delegate?.hiddenContainerViewTapped(inCell: self)
}
}
visibleContainerView
:用来存放Cell内容。
hiddenContainerView
: 用来存放左滑显示出来的视图。
相比NSLayoutConstraint
,tips40
中用到的NSLayoutAnchor
使用起来更方便一些。
例如给一个高40
的label
设置左右上边距各为20
,需要这样写:
label1.translatesAutoresizingMaskIntoConstraints = false
label1.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20).isActive = true
label1.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20).isActive = true
label1.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20).isActive = true
label1.heightAnchor.constraint(equalToConstant: 40).isActive = true
但如果使用了基于NSLayoutAnchor
的AutoLayout
扩展后可以这样
label1.layout {
$0.aTop == self.view.aTop + 20
$0.aLeading == self.view.aLeading + 20
$0.aTrailing == self.view.aTrailing - 20
$0.aHeight == 40
}
这种方式和使用Storyboard
和Xib
做AutoLayout
布局的语法很相似,简洁,可读性好。
下面再用AutoLayout
扩展举一个例子
三个label,宽度相等,高度为100,距顶部左右边距都是20。
label1.layout {
$0.aLeading == self.view.aLeading + 20
$0.aTrailing == label2.aLeading - 20
$0.aTop == self.view.aTop + 20
$0.aHeight == 100
$0.aWidth == label2.aWidth
}
label2.layout {
$0.aTop == self.view.aTop + 20
$0.aHeight == 100
$0.aTrailing == label3.aLeading - 20
$0.aWidth == label3.aWidth
}
label3.layout {
$0.aTop == self.view.aTop + 20
$0.aHeight == 100
$0.aTrailing == self.view.aTrailing - 20
}
具体代码 猛击
我们在利用UITableView
和UICollectionView
的重用机制绘制Cell
的时候经常需要先注册,然后再去复用池中取,系统给的API虽然可以达到目的,但还是有简化的空间。
- 每次注册
Cell
都需要自定义一个Identifier
,我们一般都是把这个Identifier
定义为Cell
的类名。 - 如果需要注册好几种类型的
Cell
,注册Cell
部分需要写多次。
Identifier
定义为Cell
的类名,其实就没必要每次手动配置了,我们可以直接将类名转化为Identifier
。
当需要同时注册多种类型Cell
的时候,我们可以用forEach
遍历操作。
之前的代码:
// 用Xib加载tableView的cell
tableView.register(UINib(nibName: "NibTableViewCell", bundle: nil), forCellReuseIdentifier: "NibTableViewCell")
// 纯代码加载tableView的cell
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "MyTableViewCell")
// 从复用池中取cell
let cell = tableView.dequeueReusableCell(withIdentifier: "NibTableViewCell") as! NibTableViewCell
// 同时注册多个不同的cell
tableView.register(UINib(nibName: "NibTableViewCell", bundle: nil), forCellReuseIdentifier: "NibTableViewCell")
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "MyTableViewCell")
简化之后的代码:
// 用Xib加载tableView的cell
tableView.register(cell: .nib(NibTableViewCell.self))
// 纯代码加载tableView的cell
tableView.register(cell: .class(MyTableViewCell.self))
// 从复用池中取cell
let cell: NibTableViewCell = tableView.dequeueCell(at: indexPath)
// 同时注册多个不同的cell
tableView.register(cells: [
.class(MyTableViewCell.self)
.nib(NibTableViewCell.self)
])
除了例子中的这些,还有一些UICollectionView
的扩展,参考具体实现 猛击
正则表达式具有通用性,但NSRegularExpression
使用起来并不方便,我们可以试着对它进行封装,增加一些常用的正则处理方法。
具体使用如下:
let pattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
do {
// 验证邮箱地址
let mailAddress = "darielchen@126.com"
let regex = try Regex(pattern)
if regex.matches(mailAddress) {
print("邮箱地址格式正确")
} else {
print("邮箱地址格式有误")
}
} catch {
print(error)
}
let phonePattern = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$"
do {
// 验证手机号码
let phone = "17682323553"
let regex = try Regex(phonePattern)
if regex.matches(phone) {
print("手机号格式正确")
} else {
print("手机号格式错误")
}
} catch {
print(error)
}
具体实现 猛击
如上图,常见的实现方式是把模态框作为一个View
,需要的时候通过动画从底部弹出来。这样做起来很方便,但可扩展性往往不够,弹框的内容可能会是任何控件或者组合。如果弹框是个控制器,扩展性就不会是个问题了。
如何根据文本内容的高度设置控制器的frame
?
在弹框控制器的构造方法中设置好label
的约束,然后在UIPresentationController
中重写frameOfPresentedViewInContainerView
属性,在其中通过UIView.systemLayoutSizeFitting
计算出内容的高度。
这边弹框的半径在presentationTransitionWillBegin
中设置。
具体实现 猛击
按照这个思路,我们可以自定义任何形式的弹框,包括系统的UIAlertController
的alert
和actionSheet
,下图就是自定义了系统的actionSheet
。
与上面自定义弹框不同的,自定义UIAlertController
需要把背景颜色设置为透明灰色,这个我们也是在UIPresentationController
中设置。
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
presentedView?.layer.cornerRadius = 24
containerView?.backgroundColor = .clear
// 弹框出现的时候设置透明灰度
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)
}, completion: nil)
}
}
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
// 弹框消失的时候把背景颜色置为clear
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = .clear
}, completion: nil)
}
}
这边在自定义UIAlertController
的过程中,有个bug。
当点击UIAlertController
上的确认按钮跳转到一个新的控制器,然后再返回到当前页面的时候,自定义UIAlertController
会出现一闪的情况,可以把PresentationController
中所有的代码注释掉就能重复这个bug,造成这种现象的原因是因为,在自定义尺寸的控制器上present
一个全屏控制器的时候,系统会自动把当前层级下的自定义尺寸的控制器的View
移除掉,当我们对全屏控制器做dismiss
操作后又会添加回去。
这个bug的最优解决办法是给UIPresentationController
设置一个子类,在子类中添加一个属性保存自定义尺寸的控制器的frame
。
class PresentationController: UIPresentationController {
private var calculatedFrameOfPresentedViewInContainerView = CGRect.zero
private var shouldSetFrameWhenAccessingPresentedView = false
// 如果弹框存在,设置弹框的frame
override var presentedView: UIView? {
if shouldSetFrameWhenAccessingPresentedView {
super.presentedView?.frame = calculatedFrameOfPresentedViewInContainerView
}
return super.presentedView
}
// 弹框存在
override func presentationTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
shouldSetFrameWhenAccessingPresentedView = completed
}
// 弹框消失
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
shouldSetFrameWhenAccessingPresentedView = false
}
// 获取弹框的frame
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
calculatedFrameOfPresentedViewInContainerView = frameOfPresentedViewInContainerView
}
}
具体实现 猛击
取色盘处理除了使用设计给的图片,我们还可以利用CIFilter
的CIHueSaturationValueGradient
属性来生成取色盘图片。
上图左1的实现:
let filter = CIFilter(name: "CIHueSaturationValueGradient", parameters: [
"inputColorSpace": CGColorSpaceCreateDeviceRGB(),
"inputDither": 0,
"inputRadius": 160,
"inputSoftness": 0,
"inputValue": 1
])!
let image = UIImage(ciImage: filter.outputImage!)
inputColorSpace
获取当前设备的色域。inputDither
类似像素点的一个属性,值设置为300,可以得到上图左2。inputRadius
取色盘上点的半径,上图在@2x
设备上像素点为320X320,@3x
设备上像素点为480X480。inputSoftness
柔和度,值为0表示很平滑,上图左3inputSoftness
的值为4。inputValue
表示亮度,1为最亮,0表示最暗,上图左4inputValue
的值为0.5。
获取UIImageView
对应位置的颜色
extension UIImageView {
func getPixelColorAt(point: CGPoint) -> UIColor {
let pixel = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: 4)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(
data: pixel,
width: 1,
height: 1,
bitsPerComponent: 8,
bytesPerRow: 4,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue
)
context!.translateBy(x: -point.x, y: -point.y)
layer.render(in: context!)
let color = UIColor(
red: CGFloat(pixel[0]) / 255.0,
green: CGFloat(pixel[1]) / 255.0,
blue: CGFloat(pixel[2]) / 255.0,
alpha: CGFloat(pixel[3]) / 255.0
)
pixel.deallocate()
return color
}
}
第三方库的使用可以大大减少我们开发的工作量,但也造成了第三库的代码和业务代码的高度耦合性,万一哪天使用的第三方库停止更新了,我们想要替换,成本有点大。我们就需要在业务代码和第三方库中加一个抽象层。
我们可以用分类对图片下载缓存框架KingFisher
进行隔离。
import Kingfisher
extension UIImageView {
func setImage(from url: URL) {
kf.setImage(with: url)
}
}
这样做在替换成别的图片下载缓存框架很轻松,业务中也没有必要老是导入Kingfisher
库了。
作为一个安全的存储容器Keychain
可以为不同的应用保存敏感信息,包括用户名密码啥的,苹果用它来保存Wi-Fi密码,VPN等,是一个独立于所有app之外的数据库。
而keychain-swift
就是在Keychain
基础上做了一层封装,使得我们能够更便捷的使用Keychain
。
let keychain = KeychainSwift()
keychain.set("ACCEDDTOKEN", forKey: "accessToken")
keychain.get("accessToken") // Returns "ACCEDDTOKEN"
类似key
、value
的使用方式。
在tips38
中,我们把set
和get
操作放到了属性的set
和get
,这边我们也可以这样操作。
protocol TokenStore {
var accessToken: String? { get set }
var refreshToken: String? { get set }
}
extension KeychainSwift: TokenStore {
var accessToken: String? {
get { return get(#function) }
set {
if let value = newValue {
set(value, forKey: #function)
}
}
}
var refreshToken: String? {
get { return get(#function) }
set {
if let value = newValue {
set(value, forKey: #function)
}
}
}
}
通过协议为KeychainSwift
定义了属性,可以避免直接操作字符串key
。
class AuthenticationService {
private let tokenStore: TokenStore
init(tokenStore: TokenStore) {
self.tokenStore = tokenStore
}
func fetchToken(for credentials: Credentials) {
// 保存token
// tokenStore.accessToken =
// tokenStore.refreshToken =
}
}
iOS中给App添加快捷方式的几种方案:
- 3DTouch,长按App唤起3DTouch菜单,这个同时也可以当做小组件添加到首屏左边的快捷方式页面中。
- 通过Siri唤醒对应的App。
- 直接在桌面添加对应的快捷方式,点击快捷方式直接跳到某个App的某个功能。
方案1,3DTouch的入口说实话一般人还是不太容易发现的。
方案2,跟Siri语音交互个人觉得有点蠢。
方案3,我觉得最合适了,我们用支付宝刷地铁或公交就可以通过添加桌面快捷方式,直接跳到支付二维码。
那么问题来了,支付宝是怎么做到的呢?
其实是利用了Safari
的PWA
功能,将编码好的网页内容和图标保存到桌面。点击桌面快捷方式打开网页执行JS,跳转到App对应的功能。
PWA的中文名叫渐进式网页应用。它融合了
Web
和App
的一些优点,可以在原生App的主屏幕上留下图标。可以像Native App
那样离线使用。
下面是实现步骤
在Xcode
的Target
->Info
->URL Types
的URL Schemes
添加addshortcuts
,作为跳转url
的协议头。
我们给app
中需要添加快捷方式的功能页设置好跳转url
:addshortcuts://profile
。并在AppDelegate
中添加如下代码
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let storyboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
let featureVc = storyboard.instantiateViewController(withIdentifier: "FeatureViewController")
if let navController = window?.rootViewController as? UINavigationController, let topController = navController.topViewController{
if topController.isKind(of: FeatureViewController.self) {
return true
}
if url.absoluteString == "addshortcuts://profile" {
navController.pushViewController(featureVc, animated: false)
}
}
return true
}
到这里我们可以在浏览器中输入addshortcuts://profile
,如果可以跳转到App
对应的功能页面表示一切正常。
这个引导页面支付宝做的不错,几经辗转,我拿到了这个页面,稍微修改了下,界面效果如下图
这个页面是个空白页,当我们点击快捷方式的时候,通过这个空白页跳转到App
。
<a id="redirect" href="addshortcuts://profile"></a>
打开空白页,执行下面这段JS,模拟点击上面的a标签
var element = document.getElementById('redirect');
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
document.body.style.backgroundColor = '#FFFFFF';
setTimeout(function() { element.dispatchEvent(event); }, 25);
其实引导页和跳转页可以放到一起,通过window.navigator.standalone
检测Safari
打开的Web应用程序是否全屏显示。如果是全屏显示表示是从桌面快捷方式进入的,那么就显示空白页,自动执行上面那段JS。如果不是全屏显示表示是从app
跳转过去的引导页。
Safari
可以直接打开一串包含页面内容编码的URL,这个URL
包含了这个页面需要的所有信息。
data:text/html;base64,PGEgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL0RhcmllbENoZW4vaU9TVGlwcyI+aU9TVGlwczwvYT4=
在Safari
中输入上面那串URL
,会显示一个<a href="https://github.com/DarielChen/iOSTips">iOSTips</a>
的标签。跟正常的标签一样,这是因为上面的URL
是我们经过base64
编码后处理的。同样我们可以把h5
页面转化成这种URL
编码格式。
iOS中不能用UIApplication.shared.open(url)
的方式打开包含Base64
编码的URL
,如果我们用SFSafariViewController
,它也是不能够识别这个格式的URL
。这个问题好像是出在苹果那边。
那支付宝是怎么做的呢?它是直接把这个页面部署到了服务端,我们可以参考这种方法,用Swifter
搭建一个本地的server
。
guard let deeplink = URL(string: "addshortcuts://profile") else {
return
}
guard let shortcutUrl = URL(string: "http://localhost:8244/s") else {
return
}
guard let iconData = UIImage(named: "feature_icon")?.jpegData(compressionQuality: 0.5) else {
return
}
let html = htmlFor(title: "功能快捷方式", urlToRedirect: deeplink.absoluteString, icon: iconData.base64EncodedString())
guard let base64 = html.data(using: .utf8)?.base64EncodedString() else {
return
}
server["/s"] = { request in
return .movedPermanently("data:text/html;base64,\(base64)")
}
try? server.start(8244)
整个操作流程如下图所示。
这种方式有个问题,多次添加桌面快捷方式会出现多个相同的图标,有解决方法的同学欢迎留言。
具体实现 猛击