Skip to content
记录iOS开发中的一些知识点、小技巧
Branch: master
Clone or download
Latest commit 6c163e1 Jun 19, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
Demo
MyPlayground.playground
Source
SwiftTipsDemo 54.阴影视差效果的封装 May 18, 2019
XcodeTips 文本修改 Apr 23, 2019
.gitignore initial commit Sep 28, 2018
LICENSE Initial commit Sep 28, 2018
README.md 58.恢复非正常终止的应用状态 Jun 19, 2019

README.md

iOSTips(๑•̀ㅂ•́)و✧

Language: Swift 5.0 Platform: iOS 12

1.SwiftTips

记录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的某个功能添加快捷方式
48.给UITableView添加空白页
49.线程保活的封装
50.GCD定时器
51.命名空间及应用
52.数据绑定的封装
53.基于CSS样式的富文本
54.阴影视差效果的封装
55.使用协调器模式管理控制器
56.判断字符串是否为空
57.避免将字符串硬编码在代码中
58.恢复非正常终止的应用状态

2.XcodeTips

记录使用Xcode工具的一些小技巧

1.生成对外暴露的属性和方法
2.显示Storyboard中控件之间的距离
3.重命名当前文件中的方法名或变量名
4.Storyboard中视图只覆盖不被添加
5.锁定Storyboard中控件的约束
6.多重光标操作
7.断点对象预览
8.Storyboard解耦

1.常用的几个高阶函数

函数式编程在swift中有着广泛的应用,下面列出了几个常用的高阶函数.

1. sorted

常用来对数组进行排序.顺便感受下函数式编程的多种姿势.

1. 使用sort进行排序,不省略任何类型
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]
2. 编译器可以自动推断出返回类型,所以可以省略
let sortTwoArr = intArr.sorted { (a: Int, b: Int) in
    return a < b
}
// [13, 22, 27, 45, 53, 80]
3. 编译器可以自动推断出参数类型,所以可以省略
let sortThreeArr = intArr.sorted { (a, b) in
    return a < b
}
// [13, 22, 27, 45, 53, 80]
4. 编译器可以自动推断出参数个数,所以可以用$0,$1替代
let sortFourArr = intArr.sorted {
    return $0 < $1
}
// [13, 22, 27, 45, 53, 80]
5. 如果闭包中的函数体只有一行,且需要有返回值,return可以省略
let sortFiveArr = intArr.sorted {
    $0 < $1
}
// [13, 22, 27, 45, 53, 80]
6. 最简化: 可以直接传入函数<
let sortSixArr = intArr.sorted(by: <)
// [13, 22, 27, 45, 53, 80]

2. map和compactMap

1. map: 对数组中每个元素做一次处理.
let mapArr = intArr.map { $0 * $0 }
// [169, 2025, 729, 6400, 484, 2809]
2. compactMap: 和map类似,但可以过滤掉nil,还可以对可选类型进行解包.
let optionalArr = [nil, 4, 12, 7, Optional(3), 9]
let compactMapArr = optionalArr.compactMap { $0 }
// [4, 12, 7, 3, 9]

3. filter: 将符合条件的元素重新组合成一个数组

let evenArr = intArr.filter { $0 % 2 == 0 }
// [80, 22]

4. reduce: 将数组中的元素合并成一个

// 组合成一个字符串
let stringArr = ["1", "2", "3", "*", "a"]
let allStr = stringArr.reduce("") { $0 + $1 }
// 123*a

// 求和
let sum = intArr.reduce(0) { $0 + $1 }
// 240

5. 高阶函数可以进行链式调用.比如,求一个数组中偶数的平方和

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

⬆️ 返回目录

2.高阶函数扩展

1. map函数的实现原理

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

2. 关于数组中用到的其他的一些高阶函数

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),
            ]
1. 遍历所有元素
pets.forEach { p in
   print(p.type)
}
2. 是否包含满足条件的元素
let cc = pets.contains { $0.type == "cat" }
3. 第一次出现满足条件的元素的位置
let firstIndex = pets.firstIndex { $0.age == 3 }
// 1
4. 最后一次出现满足条件的元素的位置
let lastIndex = pets.lastIndex { $0.age == 3 }
// 4
5. 根据年龄从大到小进行排序
let sortArr = pets.sorted { $0.age < $1.age }
6. 获取age大于3的元素
let arr1 = pets.prefix { $0.age > 3 }
// [{type "dog", age 5}]
7. 获取age大于3的取反的元素
let arr2 = pets.drop { $0.age > 3 }
// [{type "cat", age 3}, {type "sheep", age 1}, {type "pig", age 2}, {type "cat", age 3}]
8. 将字符串转化为数组
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!"]

⬆️ 返回目录

3.优雅的判断多个值中是否包含某一个值

我们最常用的方式

let string = "One"

if string == "One" || string == "Two" || string == "Three" {
    print("One")
}

这种方式是可以,但可阅读性不够,那有啥好的方式呢?

1. 我们可以利用contains:

if ["One", "Two", "Three"].contains(where: { $0 == "One"}) {
	print("One")
}

2. 自己手动实现一个any

使用:
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协议.

⬆️ 返回目录

4. Hashable、Equatable和Comparable协议

1. Hashable

实现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
// 哈希值

2. Equatable协议

实现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

3. Comparable协议

基于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协议除了应用在类上,还可以用在结构体枚举上.

⬆️ 返回目录

5.可变参数函数

在定义函数的时候,如果参数的个数不确定时,需要使用可变参数函数.举个例子,对数组的求和.
// 常用的姿势
[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)
    }
}

⬆️ 返回目录

6.where关键字

where的主要作用是用来做限定.

1. for循环的时候用来做条件判断

// 只遍历数组中的偶数
let arr = [11, 12, 13, 14, 15, 16, 17, 18]
for num in arr where num % 2 == 0 {
    // 12 14 16 18
}

2. 在try catch的时候做条件判断

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")
}

3. switch语句做限定条件

let student:(name:String, score:Int) = ("小明", 59)
switch student {
case let (_,score) where score < 60:
    print("不及格")
default:
    print("及格")
}

4. 限定泛型需要遵守的协议

//第一种写法
func genericFunctionA<S>(str:S) where S:ExpressibleByStringLiteral{
    print(str)
}
//第二种写法
func genericFunctionB<S:ExpressibleByStringLiteral>(str:S){
    print(str)
}

5. 为指定的类添加对应的协议扩展

// 为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

6. 为某些高阶函数的限定条件

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 关键字

⬆️ 返回目录

7.switch中判断枚举类型,尽量避免使用default

通过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
    }
}

⬆️ 返回目录

8.iOS9之后全局动态修改StatusBar样式

1. 局部修改StatusBar样式

最常用的方法是通过控制器来修改StatusBar样式

override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
}

注意:如果当前控制器有导航控制器,需要在导航控制器中这样设置(如下代码),不然不起作用.

override var preferredStatusBarStyle: UIStatusBarStyle {
    return topViewController?.preferredStatusBarStyle ?? .default
}

这样做的好处是,可以针对不同的控制器设置不同的StatusBar样式,但有时往往会多此一举,略嫌麻烦,那如何全局统一处理呢?

2. iOS9之前全局修改StatusBar样式

iOS9之前的做法比较简单,在plist文件中设置View controller-based status bar appearanceNO.

在需要设置的地方添加

UIApplication.shared.setStatusBarStyle(.default, animated: true)

这样全局设置StatusBar样式就可以了,但iOS9之后setStatusBarStyle方法被废弃了,苹果推荐使用preferredStatusBarStyle,也就是上面那种方法.

3. iOS9之后全局修改StatusBar样式

我们可以用UIAppearance和导航栏的barStyle去全局设置StatusBar的样式.

  • UIAppearance属性可以做到全局修改样式.

  • 导航栏的barStyle决定了NavigationBar的外观,而barStyle属性改变会联动到StatusBar的样式.

    1. barStyle = .default,表示导航栏的为默认样式,StatusBar的样式为了和导航栏区分,就会变成黑色.
    2. 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)
        }
    }
}

4. 怎么根据导航栏颜色自动修改StatusBar样式

在修改导航栏颜色的时候,判断下导航栏颜色的深浅

extension UIColor {
    func isDarkColor() -> Bool {
        var w: CGFloat = 0
        self.getWhite(&w, alpha: nil)
        return w > 0.5 ? false : true
    }
}

⬆️ 返回目录

9.使用面向协议实现app的主题功能

1. UIAppearance修改全局样式

做为修改全局样式的UIAppearance用起来还是很方便的,比如要修改所有UILabel的文字颜色.

UILabel.appearance().textColor = labelColor

又或者我们只想修改某个CustomView层级下的子控件UILabel

UILabel.appearance(whenContainedInInstancesOf: [CustomView.self]).textColor = labelColor

2. 主题协议,以及实现

定义好协议中需要实现的属性和方法

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() {
        // 在主题中实现相关定制功能
    }
}

3. 对主题某些样式的自定义

Demo中白色主题的UISegmentedControl需要设置特定的颜色,我们可以在LightThemeextension中重写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
    }
}

4. 主题切换

在设置完UIAppearance后需要对所有的控件进行刷新,这个操作放在apply方法中.具体实现

extension UIWindow {
    /// 刷新所有子控件
    func reload() {
        subviews.forEach { view in
            view.removeFromSuperview()
            addSubview(view)
        }
    }
}

示例Demo
实现效果

⬆️ 返回目录

10.swift中多继承的实现

1. 实现过程

swift本身并不支持多继承,但我们可以根据已有的API去实现.

swift中的类可以遵守多个协议,但是只可以继承一个类,而值类型(结构体和枚举)只能遵守单个或多个协议,不能做继承操作.

多继承的实现:协议的方法可以在该协议的extension中实现

protocol Behavior {
    func run()
}
extension Behavior {
    func run() {
        print("Running...")
    }
}

struct Dog: Behavior {}

let myDog = Dog()
myDog.run() // Running...

无论是结构体还是类还是枚举都可以遵守多个协议,所以多继承就这么做到了.

2. 通过多继承为UIView扩展方法

// 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()

3. 多继承钻石问题(Diamond Problem),及解决办法

请看下面代码

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 {}

此时ProtocolAProtocolB都有一个默认的实现方法method(),由于编译器不知道继承过来的method()方法是哪个,就会报错.

💎钻石问题,当某一个类或值类型在继承图谱中有多条路径时就会发生.

解决方法:
1. 在目标值类型或类中重写那个发生冲突的方法method().
2. 直接修改协议中重复的方法

相对来时第二种方法会好一点,所以多继承要注意,尽量避免多继承的协议中的方法的重复.

⬆️ 返回目录

11.华丽的TableView刷新动效

先看效果(由于这个页面的内容有点多,我尽量不放加载比较耗时的文件)

1. 简单的实现

我们都知道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使用这个效果该怎样封装呢?

2. 使用工厂设计模式进行封装

1. creator(创建者): Animator,用来传入参数,和设置动画
// 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)
    }
}
2. product(产品): AnimationFactory,用来设置不同的动画类型
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

⬆️ 返回目录

12.实现一个不基于Runtime的KVO

Swift并没有在语言层级上支持KVO,如果要使用必须导入Foundation框架, 被观察对象必须继承自NSObject,这种实现方式显然不够优雅.

KVO本质上还是通过拿到属性的set方法去搞事情,基于这样的原理我们可以自己去实现.

1. 实现

话不多说,直接贴代码,新建一个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差不多.

2. 使用

需要监听的类

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对象已经被释放掉了.

⬆️ 返回目录

13.实现多重代理

作为iOS开发中最常用的设计模式之一Delegate,只能是一对一的关系,如果要一对多,就只能使用NSNotification了,但我们可以有更好的解决方案,多重代理.

1. 多重代理的实现过程

1. 定义协议
protocol MasterOrderDelegate: class {
    func toEat(_ food: String)
}
2. 定义一个类: 用来管理遵守协议的类

这边用了NSHashTable来存储遵守协议的类,NSHashTableNSSet类似,但又有所不同,总的来说有这几个特点:
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)
        }
    }
}
3. 其余部分
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

2. 多重代理的应用场景

  1. IM消息接收之后在多个地方做回调,比如显示消息,改变小红点,显示消息数.
  2. UISearchBar的回调,当我们需要在多个地方获取数据的时候,类似的还有UINavigationController的回调等.

⬆️ 返回目录

14.自动检查控制器是否被销毁

检查内存泄漏除了使用Instruments,还有查看控制器popdismiss后是否被销毁,后者相对来说更方便一点.但老是盯着析构函数deinit看日志输出是否有点麻烦呢?

UIViewController有提供两个不知名的属性:

  1. isBeingDismissed: 当modal出来的控制器被dismiss后的值为true.
  2. 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报错.

⬆️ 返回目录

15.向控制器中注入代码

使用场景: 在某些控制器的viewDidLoad方法中,我们需要添加一段代码,用于统计某个页面的打开次数.

最常用的解决方案:
在父类或者extension中定义一个方法,然后在需要做统计的控制器的viewDidLoad方法中调用刚刚定义好的方法.

或者还可以使用代码注入.

1. 代码注入的使用

ViewControllerInjector.inject(into: [ViewController.self], selector: #selector(UIViewController.viewDidLoad)) {

	// $0 为ViewController对象            
	// 统计代码...
}

2.代码注入的实现

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))
    }
}

代码注入可以在不修改原有代码的基础上自定义自己所要的.相比继承,代码的可重用性会高一点,侵入性会小一点.

⬆️ 返回目录

16.给Extension添加存储属性

我们都知道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) }
    }
}

⬆️ 返回目录

17.用闭包实现按钮的链式点击事件

1. 通常姿势

通常按钮的点击事件我们需要这样写:

btn.addTarget(self, action: #selector(actionTouch), for: .touchUpInside)

@objc func actionTouch() {
    print("按钮点击事件")
}

如果有多个点击事件,往往还要写多个方法,写多了有没有觉得有点烦,代码阅读起来还要上下跳转.

2. 使用闭包封装

1. 实现
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)
        }
    }
}
2. 使用
 btn
    .addTouchUpInsideAction { btn in
        print("addTouchUpInsideAction")
    }.addTouchUpOutsideAction { btn in
        print("addTouchUpOutsideAction")
    }.addTouchDownAction { btn in
        print("addTouchDownAction")
    }
3. 实现原理

利用runtime在按钮的extension中添加一个字典属性,key对应的是事件类型,value对应的是该事件类型所要执行的闭包.然后再添加按钮的监听事件,在响应方法中,根据事件类型找到并执行对应的闭包.

链式调用就是不断返回自身.

有没有觉得如果这样做代码写起来会简洁一点呢?

⬆️ 返回目录

18.用闭包实现手势的链式监听事件

和tips17中的按钮点击事件类似,手势也可以封装成链式闭包回调.

1. 使用

view
    .addTapGesture { tap in
        print(tap)
    }.addPinchGesture { pinch in
        print(pinch)
    }

2. 实现过程

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)
        }
    }
}

具体实现 猛击

⬆️ 返回目录

19.用闭包实现通知的监听事件

1. 使用

// 通知监听
self.observerNotification(.notifyName1) { notify in
    print(notify.userInfo)
}
// 发出通知
self.postNotification(.notifyName1, userInfo: ["infoKey": "info"])

// 移除通知
self.removeNotification(.notifyName1)

2. 实现

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, &notificationActionKey)
                as? [NSNotification.Name: NotificationClosures]
        }
        set {
            objc_setAssociatedObject(self, &notificationActionKey, 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类似.

⬆️ 返回目录

20.AppDelegate解耦

作为iOS整个项目的核心App delegate,随着项目的逐渐变大,会变得越来越臃肿,一不小心代码就过了千行.

大型项目的App delegate体积会大到什么程度呢?我们可以参考下国外2亿多月活的TelegramApp delegate.是不是吓一跳,4千多行.看到这样的代码是不是很想点击左上角的x.

是时候该给App delegate解耦了,目标: 每个功能的配置或者初始化都分开,各自做各自的事情.App delegate要做到只需要调用就好了.

1.命令模式

命令模式: 发送方发送请求,然后接收方接受请求后执行,但发送方可能并不知道接受方是谁,执行的是什么操作,这样做的好处是发送方和接受方完全的松耦合,大大提高程序的灵活性.

1. 定义好协议,把相关初始化配置代码分类
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() {
        // 远程推送配置        
    }
}
2. 管理者
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()
        ]
    }
}
3. App delegate调用
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中的其他方法怎样解耦呢?

2.组合模式

组合模式: 可以将对象组合成树形结构来表现"整体/部分"层次结构. 组合后可以以一致的方法处理个别对象以及组合对象.

这边我们给App delegate每个功能模块都设置一个子类,每个子类包含所有App delegate的方法.

1. 每个子模块实现各自的功能
// 推送
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")
    }

}
2. 管理者
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)
        }
    }
}
3. App delegate调用
@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解耦相比命令模式,使用组合模式可自定义程度会更高一点.

⬆️ 返回目录

21.常见的编译器诊断指令

swift标准库提供了很多编译器诊断指令,用于在编译阶段提前处理好相关事情.

下面列出了一些常见的编译器诊断指令:

1. 代码中警告错误标识: #warning和#error

swift4.2中添加了这两个命令,再也不用在项目中自己配置错误和警告的命令了.

警告⚠️:

// Xcode会报一条黄色警告
#warning("此处逻辑有问题,明天再说")

// TODO
#warning("TODO: Update this code for the new iOS 12 APIs")

错误:

// 手动设置一条错误
#error("This framework requires UIKit!")
2. #if - #endif 条件判断
#if !canImport(UIKit)
#error("This framework requires UIKit!")
#endif

#if DEBUG
#warning("TODO: Update this code for the new iOS 12 APIs")
#endif        
3. #file、#function、#line

分别用于获取文件名,函数名称,当前所在行数,一般用于辅助日志输出.

自定义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
    }
}
4. #available和@available

一般用来判断当前代码块在某个版本及该版本以上是否可用.

if #available(iOS 8, *) {
    // iOS 8 及其以上系统运行
}

guard #available(iOS 8, *) else {
    return //iOS 8 以下系统就直接返回
}

@available(iOS 11.4, *)
func myMethod() {
    // do something
}
5. 判断是真机还是模拟器

判断是真机还是模拟器我们常用的方式是通过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

⬆️ 返回目录

22.最后执行的defer代码块

defer这个关键字不是很常用,但有时还是很有用的.

具体用法,简而言之就是,defer代码块会在函数的return前执行.

func printStringNumbers() {
    defer { print("1") }
    defer { print("2") }
    defer { print("3") }
    
    print("4")
}
printStringNumbers() // 打印 4 3 2 1

下面列举几个常见的用途:

1. try-catch
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{}底部都不会被执行.

2. 文件操作
func writeFile() {
    let file: FileHandle? = FileHandle(forReadingAtPath: filepath)
    defer { file?.closeFile() }
    
    // 文件相关操作
}

这样一方面可读性好一点,另一方面不会因为某个地方throw了一个错误而没有关闭资源文件了.

3. 避免忘记回调
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的处理逻辑混淆,代码清晰.

⬆️ 返回目录

23.定义全局常量

作为整个项目中通用的全局常量为了方便管理最好集中定义在一个地方.

下面介绍几种全局常量定义的姿势:

1. 使用结构体
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高度

好处是能比较直观的看出全局常量的定义逻辑,方便后面扩展.

2. 使用没有case的枚举

正常情况下的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,使用枚举定义常量可以避免不经意间实例化对象.

3. 使用extension

使用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)

因为传入参数类型是确定的,我们可以把类型名省略,直接用点语法.

⬆️ 返回目录

24.使用Codable协议解析JSON

swift4.0推出的Codable协议用来解析JSON还是挺不错的.

1. 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

CodableDecodableEncodable这两个协议的综合,只要遵守了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)
}
2. Codable做了哪些事情

下面我们重写系统的方法.

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.

3. Codable实际使用场景

当然了,现实开发中需要解析的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这个枚举了.

⬆️ 返回目录

25.dispatch_once替代方案

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一样的效果了.

⬆️ 返回目录

26.被废弃的+load()和+initialize()

我们都知道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()方法在这个阶段都会被调用.

⬆️ 返回目录

27.交换方法 Method Swizzling

黑魔法Method Swizzling在swift中实现的两个困难点

  • swizzling 应该保证只会执行一次.
  • swizzling 应该在加载所有类的时候调用.

分别在tips25tips26中给出了解决方案.

下面给出了两个示例供参考:

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
    }
}

SelfAwareextension中为swizzlingForClass做了默认实现,相当于一层封装.

1. 给按钮添加点击计数
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)
    }
}
2. 替换控制器的viewWillAppear方法
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")
    }
}

⬆️ 返回目录

28.获取View的指定子视图

通过递归获取指定view的所有子视图.

1. 获取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
        }
    }
}

2. 获取UIAlertController的titleLabel和messageLabel

UIAlertController好用,但可自定义程度不高,例如我们想让message文字左对齐,就需要获取到messageLabel,但UIAlertController并没有提供这个属性.

我们就可以通过递归拿到alertTitleLabelalertMessageLabel.

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
    }
}

虽然通过这种方法可以拿到alertTitleLabelalertMessageLabel.但没法区分哪个是哪个,alertTitleLabel为默认子控件的第一个label,如果title传空,message传值,alertTitleLabelalertMessageLabel获取到的都是messagelabel.

如果有更好的方法欢迎讨论.

⬆️ 返回目录

29.线程安全: 互斥锁和自旋锁(10种)

无并发,不编程.提到多线程就很难绕开锁🔐.

iOS开发中较常见的两类锁:

1. 互斥锁: 同一时刻只能有一个线程获得互斥锁,其余线程处于挂起状态.
2. 自旋锁: 当某个线程获得自旋锁后,别的线程会一直做循环,尝试加锁,当超过了限定的次数仍然没有成功获得锁时,线程也会被挂起.

自旋锁较适用于锁的持有者保存时间较短的情况下,实际使用中互斥锁会用的多一些.

1. 互斥锁,信号量

1.遵守NSLocking协议的四种锁

四种锁分别是:
NSLockNSConditionLockNSRecursiveLockNSCondition

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(),意如其名.

除此之外NSLockNSConditionLockNSRecursiveLockNSCondition四种互斥锁各有其实现:

1. 除NSCondition外,三种锁都有的两个方法:
    // 尝试去锁,如果成功,返回true,否则返回false
    open func `try`() -> Bool
    // 在limit时间之前获得锁,没有返回NO
    open func lock(before limit: Date) -> Bool
2. NSCondition条件锁:
    // 当前线程挂起
    open func wait()
    // 当前线程挂起,设置一个唤醒时间
    open func wait(until limit: Date) -> Bool
    // 唤醒在等待的线程
    open func signal()
    // 唤醒所有NSCondition挂起的线程
    open func broadcast()

当调用wait()之后,NSCondition实例会解锁已有锁的当前线程,然后再使线程休眠,当被signal()通知后,线程被唤醒,然后再给当前线程加锁,所以看起来好像wait()一直持有该锁,但根据苹果文档中说明,直接把wait()当线程锁并不能保证线程安全.

3. NSConditionLock 条件锁:

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
4. NSRecurisiveLock递归锁:

定义了可以多次给相同线程上锁并不会造成死锁的锁.

提供的几个方法和NSLock类似.

2. GCD的DispatchSemaphore和栅栏函数
1. DispatchSemaphore信号量:

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
        }
    }
}
2. barrier栅栏函数:

栅栏函数也可以做线程同步,当然了这个肯定是要并行队列中才能起作用.只有当当前的并行队列执行完毕,才会执行栅栏队列.

/// 创建并发队列
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)
    }
}
3. 其他的互斥锁
1. pthread_mutex互斥锁

pthread表示POSIX thread,跨平台的线程相关的API,pthread_mutex也是一种互斥锁,互斥锁的实现原理与信号量非常相似,阻塞线程并睡眠,需要进行上下文切换.

一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃.假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁.

这边给出了一个基于pthread_mutex_t(安全的"FIFO"互斥锁)的封装 MutexLock

1. @synchronized条件锁

日常开发中最常用的应该是@synchronized,这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁.这样,可以保证变量在作用范围内不会被其他线程改变.但是在swift中它已经不存在了.其实@synchronized在幕后做的事情是调用了objc_sync中的objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断.

因此我们可以利用闭包自己封装一套.

func synchronized(lock: AnyObject, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

// 使用
synchronized(lock: AnyObject) {
    // 此处AnyObject不会被其他线程改变
}

2. 自旋锁

1. OSSpinLock自旋锁

OSSpinLock是执行效率最高的锁,不过在iOS10.0以后已经被废弃了.

详见大神ibireme的不再安全的 OSSpinLock

2. os_unfair_lock自旋锁

它能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.这是苹果为了取代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

3. 性能比较

这边贴一张大神ibireme在iPhone6、iOS9对各种锁的性能测试图

参考:
不再安全的OSSpinLock
深入理解iOS开发中的锁

⬆️ 返回目录

30.可选类型扩展

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为空")

这是常见的几种解包方式

  1. 强制解包不太推荐使用,除非真的很确定当前可选类型不为空
  2. 可选绑定解包,虽然可以保证安全,但使用多了很容易造成层层嵌套,阅读性不好
  3. guard解包虽然能避免层层嵌套,但如果return下面还有需要执行的业务逻辑咋办
  4. ??用起来很方便,但后面只能是值,或者表达式,可能满足不了要求

其实我们可以用extensionOptional添加自定义的API。

1. isNoneisSome
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简洁一些。

2. or
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
    }  
3. on
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执行的闭包")
    })
4.其他的一些高级用法
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

⬆️ 返回目录

31.更明了的异常处理封装

// 错误类型
enum ExceptionError: Error {
    case httpCode(Int)
}

// 可能会抛出异常的方法
func throwError(code: Int) throws -> Int {
    if code == 200 {
        return code
    } else {
        throw ExceptionError.httpCode(code)
    }
}

1. 正常的处理方式

do {
    let result =  try throwError(code: 300) // 返回值
} catch {
    print(error)
}

do代码块捕捉到异常时放在catch中处理。

2. 明了的处理方式

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
    }
}

在很多情况下,这样的处理方式更方便一些。

⬆️ 返回目录

32.关键字static和class的区别

classstaticclass关键字都可以来修饰属性和方法,但它们有着本质的不同。

static关键字:它能够用在所有类型(classstructenum),表示静态方法或静态属性(计算属性和存储属性)。
class关键字:只能够用在class中,表示类方法或类属性(只能是计算属性)。

计算属性: 不直接存储值,而是提供一个gettersetter 方法来获取和设置其他属性或变量的值。
存储属性: 就是定义一个常量或者变量来存储值。

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

⬆️ 返回目录

33.在字典中用KeyPaths取值

1.[String: Any]的正常取值办法

作为一门强类型语言,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取值比起来是又臭又长,不推荐。

2.[String: Any]取值的简便方式

在取值的过程中,既然每次都要将[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去做。

2.通过KeyPath取值

如果自定义程度高一点,是不是还会有更方便的取值方式呢?我们可以参照下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
    }
}

⬆️ 返回目录

34.给UIView顶部添加圆角

之前给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
}

⬆️ 返回目录

35.使用系统自带气泡弹框

iOS中提供这几种转场样式

  • Show: 用在UINavigationController堆栈视图时,presentedViewController进入时由右向左,退出时由左向右。新压入的视图控制器有返回按钮,单击可以返回。
  • Show Detail: 只适用于嵌入在UISplitViewController对象内的视图控制器,分割控制器用以替换详细控制器,不提供返回按钮。
  • Present Modally: 有多种不同呈现方式,可根据需要设置。在iPhone中,一般以动画的形式自下向上覆盖整个屏幕。
  • Present As Popover: 在iPad中,目标视图以浮动窗样式呈现,点击目标视图以外区域,目标视图消失;在iPhone中,默认目标视图以模态覆盖整个屏幕。
  • Custom: 可自定义转场样式。

我们平时用的比较多的是ShowPresent ModallyPresent As Popover这种气泡弹出样式是用在iPad上的,但有时iPhone上会用到,我们可以做下特殊处理,不让它覆盖整个屏幕。

实现

1. 在StroyBoard中的布局

  1. 设置PopoverView控制器的尺寸。
  2. 添加segue并进行绑定。

具体步骤参考:How to popover not full screen

2. UIPopoverPresentationControllerDelegate设置

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是一个控制器,相比第三方气泡弹框,可自定义程度会高一点。

示例Demo

⬆️ 返回目录

36.给UILabel添加内边距

之前给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可以在使用StoryBoardXib时有更好的体验。

@IBDesignable修饰的类可以变得所见即所得,我们可以把cornerRadiusborderWidthborderColorshadowRadiusshadowOpacityshadowOffsetshadowColor都交给它去做。

StoryBoardXib可以达到如下图效果。

具体实现 猛击

⬆️ 返回目录

37.给UIViewController添加静态Cell

正常情况下,我们可以给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 ViewUIViewController,设置好相关尺寸。

2.删除右边的UIViewController,再添加一个UITableViewController,拖线的时候注意是Embed

然后用代理在UIViewController中操作UITableViewController

⬆️ 返回目录

38.简化使用UserDefaults

用来做简单数据存储的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,在存储和读取数据时调动的setget方法也可以交给目标属性默认的setget方法去做,。

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缓存到内存中,下次再访问时,如果内存中命中就直接访问,如果未命中再从文件中载入。它还会时不时调用同步方法来保证内存与文件中的数据的一致性,有时在写入一个值后也最好调用下这个方法来保证数据真正写入文件。

⬆️ 返回目录

39.给TabBar上的按钮添加动画

UITabBarItem中无法直接获取到按钮的UIImageViewUILabel,我们可以参照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)
    }
}

⬆️ 返回目录

40.给UICollectionView的Cell添加左滑删除

我们都知道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: 用来存放左滑显示出来的视图。

示例Demo

⬆️ 返回目录

41.基于NSLayoutAnchor的轻量级AutoLayout扩展

相比NSLayoutConstrainttips40中用到的NSLayoutAnchor使用起来更方便一些。

例如给一个高40label设置左右上边距各为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

但如果使用了基于NSLayoutAnchorAutoLayout扩展后可以这样

label1.layout {
    $0.aTop == self.view.aTop + 20
    $0.aLeading == self.view.aLeading + 20
    $0.aTrailing == self.view.aTrailing - 20
    $0.aHeight == 40
}

这种方式和使用StoryboardXibAutoLayout布局的语法很相似,简洁,可读性好。

下面再用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
}

具体代码 猛击

⬆️ 返回目录

42.简化复用Cell的代码

我们在利用UITableViewUICollectionView的重用机制绘制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的扩展,参考具体实现 猛击

⬆️ 返回目录

43.正则表达式的封装

正则表达式具有通用性,但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)
}

具体实现 猛击

⬆️ 返回目录

44.自定义带动画效果的模态框

1. 自定义弹框

如上图,常见的实现方式是把模态框作为一个View,需要的时候通过动画从底部弹出来。这样做起来很方便,但可扩展性往往不够,弹框的内容可能会是任何控件或者组合。如果弹框是个控制器,扩展性就不会是个问题了。

如何根据文本内容的高度设置控制器的frame?
在弹框控制器的构造方法中设置好label的约束,然后在UIPresentationController中重写frameOfPresentedViewInContainerView属性,在其中通过UIView.systemLayoutSizeFitting计算出内容的高度。

这边弹框的半径在presentationTransitionWillBegin中设置。

具体实现 猛击

2. 自定义UIAlertController

按照这个思路,我们可以自定义任何形式的弹框,包括系统的UIAlertControlleralertactionSheet,下图就是自定义了系统的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
    }
}

具体实现 猛击

⬆️ 返回目录

45.利用取色盘获取颜色

1.生成取色盘

取色盘处理除了使用设计给的图片,我们还可以利用CIFilterCIHueSaturationValueGradient属性来生成取色盘图片。

上图左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。

2.获取颜色

获取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
    }
}

⬆️ 返回目录

46.第三方库的依赖隔离

第三方库的使用可以大大减少我们开发的工作量,但也造成了第三库的代码和业务代码的高度耦合性,万一哪天使用的第三方库停止更新了,我们想要替换,成本有点大。我们就需要在业务代码和第三方库中加一个抽象层。

Extensions

我们可以用分类对图片下载缓存框架KingFisher进行隔离。

import Kingfisher

extension UIImageView {
    func setImage(from url: URL) {
        kf.setImage(with: url)
    }
}

这样做在替换成别的图片下载缓存框架很轻松,业务中也没有必要老是导入Kingfisher库了。

Protocols

作为一个安全的存储容器Keychain可以为不同的应用保存敏感信息,包括用户名密码啥的,苹果用它来保存Wi-Fi密码,VPN等,是一个独立于所有app之外的数据库。

keychain-swift就是在Keychain基础上做了一层封装,使得我们能够更便捷的使用Keychain

let keychain = KeychainSwift()
keychain.set("ACCEDDTOKEN", forKey: "accessToken")
keychain.get("accessToken") // Returns "ACCEDDTOKEN"

类似keyvalue的使用方式。

tips38中,我们把setget操作放到了属性的setget,这边我们也可以这样操作。

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 =
    }
}

⬆️ 返回目录

47.给App的某个功能添加快捷方式

iOS中给App添加快捷方式的几种方案:

  1. 3DTouch,长按App唤起3DTouch菜单,这个同时也可以当做小组件添加到首屏左边的快捷方式页面中。
  2. 通过Siri唤醒对应的App。
  3. 直接在桌面添加对应的快捷方式,点击快捷方式直接跳到某个App的某个功能。

方案1,3DTouch的入口说实话一般人还是不太容易发现的。
方案2,跟Siri语音交互个人觉得有点蠢。
方案3,我觉得最合适了,我们用支付宝刷地铁或公交就可以通过添加桌面快捷方式,直接跳到支付二维码。

那么问题来了,支付宝是怎么做到的呢?

其实是利用了SafariPWA功能,将编码好的网页内容和图标保存到桌面。点击桌面快捷方式打开网页执行JS,跳转到App对应的功能。

PWA的中文名叫渐进式网页应用。它融合了WebApp的一些优点,可以在原生App的主屏幕上留下图标。可以像Native App那样离线使用。

下面是实现步骤

1. 配置App跳转的URL

XcodeTarget->Info->URL TypesURL 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对应的功能页面表示一切正常。

2. 设置添加快捷方式到桌面的引导H5页面和跳转到AppH5页面

1. 引导添加桌面快捷方式的H5页面

这个引导页面支付宝做的不错,几经辗转,我拿到了这个页面,稍微修改了下,界面效果如下图

2. 跳转到AppH5页面

这个页面是个空白页,当我们点击快捷方式的时候,通过这个空白页跳转到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跳转过去的引导页。

3. 搭建可以打开编码后H5页面的本地server

1. h5编码

Safari可以直接打开一串包含页面内容编码的URL,这个URL包含了这个页面需要的所有信息。

data:text/html;base64,PGEgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL0RhcmllbENoZW4vaU9TVGlwcyI+aU9TVGlwczwvYT4=

Safari中输入上面那串URL,会显示一个<a href="https://github.com/DarielChen/iOSTips">iOSTips</a>的标签。跟正常的标签一样,这是因为上面的URL是我们经过base64编码后处理的。同样我们可以把h5页面转化成这种URL编码格式。

2. 搭建本地server

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)

4. 总结

整个操作流程如下图所示。

这种方式有个问题,多次添加桌面快捷方式会出现多个相同的图标,有解决方法的同学欢迎留言。

具体实现 猛击

⬆️ 返回目录

48.给UITableView添加空白页

介绍一种简单的给UITableView设置空白页的方法。

extension UITableView {

    /// 添加空白页
    ///
    /// - Parameter message: 空白页文字
    func setNoDataPlaceholder(_ message: String) {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        label.text = message
        label.textAlignment = .center
        label.sizeToFit()
        self.isScrollEnabled = false
        self.backgroundView = label
        self.separatorStyle = .none
    }
    
    /// 删除空白页
    func removeNoDataPlaceholder() {
        self.isScrollEnabled = true
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
}

上面代码只支持纯文本的空白页提示,如果需要添加空白页图片可以在setNoDataPlaceholder方法中自定义。

为了方便在RxSwift中使用,我们可以给Reactive添加扩展

extension Reactive where Base: UITableView {
    func isEmpty(message: String) -> Binder<Bool> {
        return Binder(base) { tableView, isEmpty in
            if isEmpty {
                tableView.setNoDataPlaceholder(message)
            } else {
                tableView.removeNoDataPlaceholder()
            }
        }
    }
}

使用

let isEmpty = tableView.rx.isEmpty(message:"暂无数据")
viewModel.responses.map({ $0.count <= 0 }).distinctUntilChanged().bind(to: isEmpty).disposed(by: disposeBag)

⬆️ 返回目录

49.线程保活的封装

当我们开启一个线程并执行完相关的操作后,线程就会立即被销毁。下次再执行同样的操作时,还会创建一个新的线程,这就产生了不必要的开销,尤其是这样的操作很频繁的时候。

如何在执行完相关的操作后,线程依然不被销毁,等到下次直接去使用呢?

我们可以在这个线程中创建一个RunLoop,由RunLoop去管理线程。

在上代码前先解释下RunLoop和线程之间的关系

  1. 每个线程都有一个对应的RunLoop,主线程的RunLoopMain函数中创建。
  2. 主线程的RunLoop是自动开启的,子线程默认并没有开启RunLoop
  3. 子线程创建的时候并没有创建好RunLoop对象,RunLoop对象会在第一次获取它的时候创建。
  4. 线程中的RunLoop会和线程一起被销毁。

RunLoop管理线程:

    innerThread = DCThread(block: { [weak self] in
        // RunLoop.current:如果当前RunLoop不存在,就在线程中创建一个RunLoop
        // 往RunLoop中添加Source,防止空的RunLoop自动退出
        RunLoop.current.add(Port(), forMode: RunLoop.Mode.default)
        while self?.isStopped == false {
            // 单次运行RunLoop,不会超时
            RunLoop.current.run(mode: RunLoop.Mode.default, before: NSDate.distantFuture)
        }
    })
    innerThread?.start()

在子线程中执行任务:

    self.perform(#selector(innerExecuteTask(task:)), on: innerThread, with: task, waitUntilDone: false)

    @objc private func innerExecuteTask(task: @escaping @convention(block)() -> Void) {
        // 需要在子线程中执行的操作
    }

停止子线程的RunLoop,销毁子线程:

    self.perform(#selector(innerStop), on: innerThread, with: nil, waitUntilDone: true)

    @objc private func innerStop() {
        // 终止RunLoop的while循环
        isStopped = true
        // 停止当前线程中的当次RunLoop
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.innerThread = nil
    }

封装完之后使用

    // 创建
    let thread = PermenantThread()

    // 使用
    thread.executeTask {
        // 需要在子线程中执行的操作
        print(Thread.current)
    }

    // 销毁
    thread.stop()

具体实现 猛击

⬆️ 返回目录

50.GCD定时器

CADisplayLinkNSTimer的底层都是基于RunLoop实现的,而主线程的RunLoop承担了大量的工作,比如UI界面的刷新,点击事件的处理,动画的响应等,如果RunLoop的任务过于繁重,就可能导致CADisplayLinkNSTimer执行的时候不准时。

在设置好CADisplayLinkNSTimer定时器后,RunLoop会每跑一圈去确认下时间,RunLoop每次循环的任务可能不一样,如果当次循环还没执行到定时器执行的时间间隔,就会执行下一次RunLoop循环, 而下一次循环执行到定时器任务时可能已经超过了定时器执行的时间间隔,这就导致了基于RunLoop的定时器的不准确。

而基于系统内核的GCD定时器不依赖RunLoop,相对来说会更准确一些。

为了方便使用,下面给出了GCD定时器定时器的封装

class GCDTimer {
    
    public let dispatchSourceTimer : DispatchSourceTimer
    
    init(in : GCDQueue, delay : Float = 0, interval : Float) {
        dispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: `in`.dispatchQueue)
        dispatchSourceTimer.schedule(deadline: .now() + .milliseconds(Int(delay * 1000)), repeating: .milliseconds(Int(interval * 1000)))
    }
    
    /// 设定定时器任务的回调函数
    ///
    /// - Parameter eventHandler: 回调函数
    func setTimerEventHandler(eventHandler: @escaping (GCDTimer) -> Void) {
        dispatchSourceTimer.setEventHandler {
            eventHandler(self)
        }
    }
    
    /// 设定定时器销毁时候的回调函数
    ///
    /// - Parameter eventHandler: 回调函数
    func setDestroyEventHandler(eventHandler: @escaping () -> Void) {
        dispatchSourceTimer.setCancelHandler {
            eventHandler()
        }
    }
    
    /// 挂起
    func suspend() {
         dispatchSourceTimer.suspend()
    }
    
    /// 开始定时
    func start() {
         dispatchSourceTimer.resume()
    }

    /// 定时器销毁(执行了此方法后,start就会变得无效)
    func destroy() {
         dispatchSourceTimer.cancel()
    }
}

使用

// 设置定时器,延迟2秒执行,间隔1秒
let gcdTimer = GCDTimer(in: GCDQueue.global(), delay: 2, interval: 1)
    
var count : Int = 0
// 执行事件
gcdTimer.setTimerEventHandler { _ in
    count += 1
    print("\(count)")
    
    if count == 8 {
        // 定时器销毁
        gcdTimer.destroy()
    }
}
// 定时器开始
gcdTimer.start()
    
gcdTimer.setDestroyEventHandler {
    print("销毁事件的回调")
}

具体实现 猛击

⬆️ 返回目录

51.命名空间及应用

在OC中因为没有命名空间,开发者在给类库命名的时候一般都会加上两三个字母的前缀来防止命名冲突。这样做虽然可以大大降低引用第三方类库时的冲突,但还是会碰到命名相同的情况。

而在swift中由于可以使用命名空间,即使相同的类型名称只要来自不同的模块,就可以共存。每个模块都有一个自己的命名空间。

1. 如何避免与系统API命名冲突

避免命名冲突最好的方式是跟系统的命名不一样,这也是代码规范的一部分,但如果真的冲突了,怎么解决?

// 获取当前项目的命名空间
let nameSpace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String // Optional("Namespace")

// 正常使用Int类型
let zeroOrOne = Int.random(in: 0...1)
print(zeroOrOne)

自定义了一个Int结构体

struct Int {}

let zeroOrOne = Int.random(in: 0...1) // 报错: Type 'Int' has no member 'random'

上面代码中的Int.random其实省略了命名空间,完整的写法应该是这样

// Namespace是当前项目的命名空间
let zeroOrOne = Namespace.Int.random(in: 0...1) // 报错: Type 'Int' has no member 'random'

如果我们想用Foundation中的Int类型应该在前面加Swift前缀。使用UIKit中的API,前面默认是加的UIKit

let zeroOrOne = Swift.Int.random(in: 0...1)
print(zeroOrOne) // 0 或 1

let kitView = UIKit.UIView()

2. 如何避免模块之间的命名冲突

在模块A和模块B中分别定义了两个相同名字的结构体Int,如果我们要在项目中同时使用这两个结构体只需要加前缀。

import ModuleA
import ModuleB

print(ModuleA.Int().description) // ModuleA
print(ModuleB.Int().description) // ModuleB

3. 伪命名空间

有时为了更清晰的代码结构往往需要多层嵌套,我们可以使用伪命名空间来使代码结构更清晰。

用枚举来做伪命名空间嵌套更合适一些。因为相比类和结构体,枚举没有构造方法,也没有类的继承操作。

1. 利用伪命名空间定义全局常量
class ItemListViewController { ... }
extension ItemListViewController {

    enum Constants {
        static let itemsPerPage = 7
        static let headerHeight: CGFloat = 60
    }
}

// 全局常量
ItemListViewController.Constants.itemsPerPage
ItemListViewController.Constants.headerHeight

这样嵌套定义的全局常量一方面是直观清晰,另一方面可以有效避免命名污染。

2. 利用伪命名空间来设置工厂方法
struct Item {
    ...
}
extension Item {
    enum Factory {
        static func make(from anotherItem: AnotherItem) -> Item {
            // 相关操作
            return Item(...)
        }
    }
}

let anotherItem = AnotherItem()
let item = Item.Factory.make(from: anotherItem)

总结 :命名空间可以有效避免不同模块之间的命名冲突,但同一模块中的命名冲突并不支持,我们可以利用伪命名空间的嵌套来避免。

⬆️ 返回目录

52.数据绑定的封装

函数响应式编程(例如RxSwift)有一个很大的优势,可以将模型与视图绑定。当模型中数据发生改变后,视图会直接产生变化,不用再去刷新视图。

下面给出了一个简单的数据绑定封装,不再依赖RxSwift

使用:

// 模型
struct User {
    var name: String
    var followersCount: Int
}

class TestViewController: UIViewController {
    
    private lazy var countLabel = UILabel()
    var user: Bindable<User>? = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 创建模型
        user = Bindable(User(name: "Dariel", followersCount: 5))
        
        countLabel.text = "0"
        view.addSubview(countLabel)
        
        // 添加观察者,只要模型数据发生改变就会进回调
        user?.addObservation(for: self) { [weak self] (vc, user) in
            
            self?.countLabel.text = String(user.followersCount)
            self?.countLabel.sizeToFit()
            self?.countLabel.center = (self?.view.center)!
        }
        // 模型与视图绑定,本质上是添加观察者方法的又一层封装
        user?.bind(\.name, to: navigationItem, \.title)
        
        view.backgroundColor = UIColor.groupTableViewBackground
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        // 改变模型
        user?.lastValue.followersCount += 1
        user?.lastValue.name = "大雷"
    }
    
    deinit {
        print("销毁.....")
    }
}

实现:

class Bindable<Value> {
    private var observations = [(Value) -> Bool]()
    // 模型
    var lastValue: Value {
        didSet { update() }
    }
    
    init(_ value: Value) {
        lastValue = value
    }
    
    /// 添加观察者
    ///
    /// - Parameters:
    ///   - object: 观察对象
    ///   - handler: 回调
    func addObservation<O: AnyObject>(for object: O, handler: @escaping (O, Value) -> Void) {
        handler(object, lastValue)
        observations.append { [weak object] value in
            guard let object = object else {
                return false
            }
            handler(object, value)
            return true
        }
    }
    
    /// 更新数据
    ///
    /// - Parameter value: 模型
    func update(with value: Value? = nil) {
        observations = observations.filter {
            $0(value ?? lastValue) }
    }
    
    /// 绑定视图
    ///
    /// - Parameters:
    ///   - sourceKeyPath: 模型属性
    ///   - object: 视图
    ///   - objectKeyPath: 视图属性
    func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T?>) {
        
        addObservation(for: object) { object, observed in
            let value = observed[keyPath: sourceKeyPath]
            object[keyPath: objectKeyPath] = value
        }
    }
}

⬆️ 返回目录

53.基于CSS样式的富文本

iOS对于富文本的操作一直不是那么方便,一般涉及到富文本的页面我们会优先考虑使用WebViewCSS一方面样式丰富,另一方面是写起来简单。

其实NSAttributedString也是支持带有CSS样式的文本的,其DocumentType有一个类型为html

下面给出了一个富文本的封装,可以便捷操作NSAttributedString

extension NSAttributedString {
    
    convenience init(text: String, styles: [String: String]) {
        
        // 字典转CSS样式字符串
        let style = styles.compactMap({ (property, value) -> String in
            return "\(property): \(value)"
        }).joined(separator: ";")
        
        // 生成span标签对应的富文本
        try! self.init(
            data: Data("<span style=\"\(style)\">\(text)</span>".utf8),
            options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue],
            documentAttributes: nil
        )
    }
    
    /// 可以通过"+"拼接NSAttributedString对象
    static func +(lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString {
        
        let concatenatedString = NSMutableAttributedString(attributedString: lhs)
        concatenatedString.append(rhs)
        return concatenatedString
    }
}

使用:

绘制一个如下图的Label

// 统一的样式
enum Styles {

    static let `protocol` = [
        "color" : "#7B68EE",
        "font-size": "32px",
        "font-style": "italic",
    ]
    
    static let separator = [
        "color" : "#B22222",
        "font-size": "28px",
    ]
    
    static let hostname = [
        "color" : "#20B2AA",
        "font-size": "34px",
        "text-decoration": "underline",
    ]
    
    static let path = [
        "color" : "#FFDEAD",
        "font-size": "32px",
        "text-decoration": "underline",
        "font-style": "oblique",
    ]
}


let `protocol` = NSAttributedString(text: "https", styles: Styles.protocol)
let separator = NSAttributedString(text: "://", styles: Styles.separator)
let hostname = NSAttributedString(text: "github.com", styles: Styles.hostname)
let path = NSAttributedString(text: "/DarielChen/iOSTips", styles: Styles.path)
        
label.attributedText = `protocol` + separator + hostname + path

⬆️ 返回目录

54.阴影视差效果的封装

iPhone锁屏页面上下左右翻转iPhone,锁屏壁纸会沿着各个方向发生位移,有一个独特的视差效果。这是利用了陀螺仪,好在苹果有相关的API支持,使用起来不是那么复杂。

我们可以把这个API用在自己的项目中,比如改变阴影的位移。

    blueView.cornerRadius = 6
    blueView.borderWidth = 2
    blueView.borderColor = .lightGray
       
    blueView.shadowColor = UIColor.darkGray
    blueView.shadowRadius = 6
    blueView.shadowOpacity = 0.5
    blueView.shadowOffset = CGSize(width: 8, height: 8)
    
    // 基于位置的偏移    
    // blueView.shadowMotionOffset = CGSize(width: 12, height: 12)
    // 阴影的偏移
    blueView.motionOffset = CGSize(width: 12, height: 12)

具体实现 猛击

⬆️ 返回目录

55.使用协调器模式管理控制器

对于控制器之间的跳转,最方便的操作是在需要跳转的地方创建目标控制器,但这种方式总觉得不是太合适。

  • 当前控制器和目标控制器本质上是同一类型对象,不应该在当前控制器的某个方法中创建目标控制器。
  • 控制器的创建分散在控制器中,不便于统一管理和模块化开发。

使用协调器模式就可以解决上面两个问题。

使用协调器模式的优点:

  1. 将控制器的创建,配置和切换从控制器层级中剥离开,放到协调器层级中
  2. 控制器不再通过导航控制器pushpresent目标控制器
  3. 控制器之间的结构更清晰,易于调试
  4. 协调器中可以嵌套子协调器,使减小模块与模块之间的耦合度

当然了协调器模式也会有缺点:

  1. 协调器不可避免会带来更多的代码,很多操作都要使用代理
  2. 有时还会需要多级代理来传递数据

虽然有缺点但协调器模式还是值得一用。

协调器模式的使用流程

// 所有的协调器遵守的协议
protocol Coordinator {
    // 设置协调器初始模块
    func start()
}
1. 主协调器

主协调器AppCoordinator负责管理所有的子协调器,并设置所有子协调器的代理为自身,实现代理方法,作为子协调器之间的切换操作。

2. 统一管理用Storyboard创建的控制器
extension UIStoryboard {
    
    // MARK: - 获取对应的Storyboard
    private static var main: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: nil)
    }
    // MARK: - Storyboard中控制器管理
    static func instantiateMainViewController(delegate: MainViewControllerDelegate) -> MainViewController {
        let mainVc = main.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController
        mainVc.delegate = delegate
        return mainVc
    }
}

利用extension加了一层封装,用起来方便一些。

3. 子协调器

子协调器的实现类似主协调器,每个子协调器负责管理一个模块,模块内部的控制器跳转交给子协调器管理,主协调器管理所有的子协调器。实例代码中有两个子模块AuthMain

具体实现 猛击

⬆️ 返回目录

56.判断字符串是否为空

这个空可能是nil 单个空格 多个空格 tab return 全角空格

1. 使用isEmpty

isEmpty无法判断多个空字符串组合的情况。

 print("str".isEmpty)  // false
 print("".isEmpty)     // true
 print("  ".isEmpty)   // false

因为isEmpty的实现逻辑是基于startIndexendIndex。下面是Collection.swift中关于isEmpty的实现过程。

public var isEmpty: Bool {
  return startIndex == endIndex
}

isEmpty不能满足需要。

2. 判断字符串中是否包含空格

isWhitespace可以实现,但每一次都需要遍历这个字符串。这个遍历我们可以用allSatisfy这个高阶函数实现,allSatisfy只有当所有的元素满足条件才会返回true,否则返回false

extension String {
    var isBlank: Bool {
        return allSatisfy({ $0.isWhitespace })
    }
}

"str".isBlank          // false
"   str   ".isBlank    // false
"".isBlank             // true
" ".isBlank            // true
"\t\r\n".isBlank       // true
"\u{00a0}".isBlank     // true

需要判断字符串是否为空,我们为可选类型添加扩展

extension Optional where Wrapped == String {
    var isBlank: Bool {
        return self?.isBlank ?? true
    }
}

var str: String? = nil
str.isBlank            // true
str = ""               
str.isBlank            // true
str = "  \t  "               
str.isBlank            // true
str = "Dariel"
str.isBlank            // false

⬆️ 返回目录

57.避免将字符串硬编码在代码中

之前都是直接把字符串写在代码中的,思考了一下觉得代码在整洁性和灵活性上有欠缺,其实可以做下统一管理。项目后期要是需要添加国际化也方便一些。

但如果整个项目的字符串都做统一管理,在开发效率上就会有折扣,所以决定在每个包含字符串的文件中添加String Extension

fileprivate extension String {
    
    static let recordCell = "recordCell"
    static let showPlayer = "showPlayer"
    static let uuidPathKey = "uuidPath"
    
    static let submit = NSLocalizedString("提交", comment: "提交按钮的标题")
    static let pause = NSLocalizedString("暂停", comment: "")
    static let save = NSLocalizedString("保存", comment: "")
}

对于显示在UI上的文字使用NSLocalizedString

⬆️ 返回目录

58.恢复非正常终止的应用状态

App 进入后台后,长时间不使用,或者系统觉得内存不够了,会被自动清理掉。下次再启动的时候,原先的页面和在页面上的操作没有了,用户体验不友好。其实苹果提供了该问题的解决方案。

冷启动恢复状态分为两步:

  1. 恢复之前栈中存在的控制器
  2. 恢复页面上的内容

1. 控制器栈的恢复

1. 在AppDelegate中实现两个方法
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {

    return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {

    return true
}
2. 设置Restoration ID
1. 对于使用xib或者Storyboard创建的控制器,需要在如下图添加Restoration ID

2.对于纯代码创建的控制器

需要遵守UIViewControllerRestoration协议,并实现withRestorationIdentifierPath方法,在这个方法中初始化目标控制器。

extension LastViewController: UIViewControllerRestoration {
    static func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
        let vc = LastViewController()
        return vc
    }
}

2. 控制器状态的恢复

例子,恢复控制器中的一个UITextField输入文本。

extension SecondViewController {

    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with: coder)

        guard let input = inputField.text, isViewLoaded else {
            return
        }
        coder.encode(input, forKey: .encodingKeyFieldValue)
    }

    override func decodeRestorableState(with coder: NSCoder) {
        super.decodeRestorableState(with: coder)
        assert(isViewLoaded, "We assume the controller is never restored without loading its view first.")
        inputField.text = coder.decodeObject(forKey: .encodingKeyFieldValue) as? String
    }

    override func applicationFinishedRestoringState() {
        print("Finished restoring everything.")
    }
}

fileprivate extension String {
    static let encodingKeyFieldValue = "encodingKeyFieldValue"
}

3.调试

  1. 使用Xcode将项目在真机或模拟器上跑起来
  2. Home键将项目退到后台
  3. 点击Xcode的停止项目按钮
  4. 打开非正常终止的项目

具体实现 猛击

⬆️ 返回目录

You can’t perform that action at this time.