如何系统地自学 Swift 语言并学会 iOS 开发?

大学刚毕业,出来工作了一段时间,感觉这样下去的话,前途渺茫。必须要学一门技术,通过同学的建议,开始接触编程,自己有部肾6s,想学 iOS 开发,想从 …
关注者
878
被浏览
344,412
登录后你可以
不限量看优质回答 私信答主深度交流 精彩内容一键收藏

update:内容已更新到 4.0 版本。十五万字的手册值得你来收藏!和分享,完美。

背景说明

越来越多同学打算开始用 Swift 来开发了,可很多人以前都没接触过 Swift。这篇和我以前文章不同的是,本篇只是面向 Swift 零基础的同学,内容主要是一些直接可用的小例子,例子可以直接在工程中用或自己调试着看。

记得以前 PHP 有个 chm 的手册,写的很简单,但很全,每个知识点都有例子,社区版每个知识点下面还有留言互动。因此,我弄了个 Swift 的手册,是个 macOS 程序。建议使用我开发的这个 macOS 程序来浏览。源码地址: ming1016/SwiftPamphletApp ,直接下载 dmg 地址: 戴铭的Swift小册子4.1.dmg.zip

这个程序是Swift写的,按照声明式UI,响应式编程范式开发的,源码也可以看看。与其讲一堆,不如调着试。

下面是文本内容。注:代码中简化变量名是为了能更快速关注到语言用法。

语法速查

基础

变量 let, var

变量是可变的,使用 var 修饰,常量是不可变的,使用 let 修饰。类、结构体和枚举里的变量是属性。

var v1:String = "hi" // 标注类型
var v2 = "类型推导"
let l1 = "标题" // 常量
class a {
    let p1 = 3
    var p2: Int {
        p1 * 3
}

属性没有 set 可以省略 get,如果有 set 需加 get。变量设置前通过 willSet 访问到,变量设置后通过 didSet 访问。

打印 print("")

控制台打印值

print("hi")
let i = 14
print(i)
print("9月\(i)是小柠檬的生日")
for i in 1...3{
    print(i)
// output:
// 使用terminator使循环打印更整洁
for i in 1...3 {
    print("\(i) ", terminator: "")
// output:
// 1 2 3

注释 //

// 单行注释
多行注释第一行。
多行注释第二行。
// MARK: 会在 minimap 上展示
// TODO: 待做
// FIXME: 待修复

可选 ?, !

可能会是 nil 的变量就是可选变量。当变量为 nil 通过??操作符可以提供一个默认值。

var o: Int? = nil
let i = o ?? 0

闭包

闭包也可以叫做 lambda,是匿名函数,对应 OC 的 block。

let a1 = [1,3,2].sorted(by: { (l: Int, r: Int) -> Bool in
    return l < r
// 如果闭包是唯一的参数并在表达式最后可以使用结尾闭包语法,写法简化为
let a2 = [1,3,2].sorted { (l: Int, r: Int) -> Bool in
    return l < r
// 已知类型可以省略
let a3 = [1,3,2].sorted { l, r in
    return l < r
// 通过位置来使用闭包的参数,最后简化如下:
let a4 = [1,3,2].sorted { $0 < $1 }

函数也是闭包的一种,函数的参数也可以是闭包。@escaping 表示逃逸闭包,逃逸闭包是可以在函数返回之后继续调用的。@autoclosure 表示自动闭包,可以用来省略花括号。

函数 func

函数可以作为另一个函数的参数,也可以作为另一个函数的返回。函数是特殊的闭包,在类、结构体和枚举中是方法。

// 为参数设置默认值
func f1(p: String = "p") -> String {
    "p is \(p)"
// 函数作为参数
func f2(fn: (String) -> String, p: String) -> String {
    return fn(p)
print(f2(fn:f1, p: "d")) // p is d
// 函数作为返回值
func f3(p: String) -> (String) -> String {
    return f1
print(f3(p: "yes")("no")) // p is no

函数可以返回多个值,函数是可以嵌套的,也就是函数里内可以定义函数,函数内定义的函数可以访问自己作用域外函数内的变量。inout 表示的是输入输出参数,函数可以在函数内改变输入输出参数。defer 标识的代码块会在函数返回之前执行。

函数在 Swift 5.4 时开始有了使用多个变量参数的能力,使用方法如下:

func f4(s: String..., i: Int...) {
    print(s)
    print(i)
f4(s: "one", "two", "three", i: 1, 2, 3)
/// ["one", "two", "three"]
/// [1, 2, 3]

嵌套函数可以重载,嵌套函数可以在声明函数之前调用他。

func f5() {
    nf5()
    func nf5() {
        print("this is nested function")
f5() // this is nested function

访问控制

在 Xcode 里的 target 就是模块,使用 import 可导入模块。模块内包含源文件,每个源文件里可以有多个类、结构体、枚举和函数等多种类型。访问级别可以通过一些关键字描述,分为如下几种:

  • open:在模块外可以调用和继承。
  • public:在模块外可调用不可继承,open 只适用类和类成员。
  • internal:默认级别,模块内可跨源文件调用,模块外不可调用。
  • fileprivate:只能在源文件内访问。
  • private:只能在所在的作用域内访问。

重写继承类的成员,可以设置成员比父类的这个成员更高的访问级别。Setter 的级别可以低于对应的 Getter 的级别,比如设置 Setter 访问级别为 private,可以在属性使用 private(set) 来修饰。

基础类型

数字 Int, Float

数字的类型有 Int、Float 和 Double

// Int
let i1 = 100
let i2 = 22
print(i1 / i2) // 向下取整得 4
// Float
let f1: Float = 100.0
let f2: Float = 22.0
print(f1 / f2) // 4.5454545
let f3: Float16 = 5.0 // macOS 还不能用
let f4: Float32 = 5.0
let f5: Float64 = 5.0
let f6: Float80 = 5.0
print(f4, f5, f6) // 5.0 5.0 5.0
// Double
let d1: Double = 100.0
let d2: Double = 22.0
print(d1 / d2) // 4.545454545454546
// 字面量
print(Int(0b10101)) // 0b 开头是二进制 
print(Int(0x00afff)) // 0x 开头是十六进制
print(2.5e4) // 2.5x10^4 十进制用 e
print(0xAp2) // 10*2^2  十六进制用 p
print(2_000_000) // 2000000
// isMultiple(of:) 方法检查一个数字是否是另一个数字的倍数
let i3 = 36
print(i3.isMultiple(of: 9)) // true

处理数字有 floor、ceil、round。floor 是向下取整,只取整数部分;cell 是向上取整,只要有不为零的小数,整数就加1;round 是四舍五入。

布尔数 Bool

布尔数有 true 和 false 两种值,还有一个能够切换这两个值的 toggle 方法。

var b = false
b.toggle() // true
b.toggle() // false

元组 (a, b, c)

元组里的值类型可以是不同的。元组可以看成是匿名的结构体。

let t1 = (p1: 1, p2: "two", p3: [1,2,3])
print(t1.p1)
print(t1.p3)
// 类型推导
let t2 = (1, "two", [1,2,3])
// 通过下标访问
print(t2.1) // two
// 分解元组
let (dp1, dp2, _) = t2
print(dp1)
print(dp2)

字符串

let s1 = "Hi! This is a string. Cool?"
/// 转义符 \n 表示换行。
/// 其它转义字符有 \0 空字符)、\t 水平制表符 、\n 换行符、\r 回车符
let s2 = "Hi!\nThis is a string. Cool?"
// 多行
let s3 = """
This is a string.
Cool?
// 长度
print(s3.count)
print(s3.isEmpty)
// 拼接
print(s3 + "\nSure!")
// 字符串中插入变量
let i = 1
print("Today is good day, double \(i)\(i)!")
/// 遍历字符串
/// 输出:
/// o
/// n
/// e
for c in "one" {
    print(c)
// 查找
print(s3.lowercased().contains("cool")) // true
// 替换
let s4 = "one is two"
let newS4 = s4.replacingOccurrences(of: "two", with: "one")
print(newS4)
// 删除空格和换行
let s5 = " Simple line. \n\n  "
print(s5.trimmingCharacters(in: .whitespacesAndNewlines))
// 切割成数组
let s6 = "one/two/three"
let a1 = s6.components(separatedBy: "/") // 继承自 NSString 的接口
print(a1) // ["one", "two", "three"]
let a2 = s6.split(separator: "/")
print(a2) // ["one", "two", "three"] 属于切片,性能较 components 更好
// 判断是否是某种类型
let c1: Character = " "
print(c1.isASCII) // false
print(c1.isSymbol) // true
print(c1.isLetter) // false
print(c1.isNumber) // false
print(c1.isUppercase) // false
// 字符串和 Data 互转
let data = Data("hi".utf8)
let s7 = String(decoding: data, as: UTF8.self)
print(s7) // hi
// 字符串可以当作集合来用。
let revered = s7.reversed()
print(String(revered))

Unicode、Character 和 SubString 等内容参见官方字符串文档: Strings and Characters — The Swift Programming Language (Swift 5.1)

字符串字面符号可以参看《 String literals in Swift 》。

原始字符串

// 原始字符串在字符串前加上一个或多个#符号。里面的双引号和转义符号将不再起作用了,如果想让转义符起作用,需要在转义符后面加上#符号。
let s8 = #"\(s7)\#(s7) "one" and "two"\n. \#nThe second line."#
print(s8)
/// \(s7)hi "one" and "two"\n.
/// The second line.
// 原始字符串在正则使用效果更佳,反斜杠更少了。
let s9 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
let s10 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#
print(s9) // \\[A-Z]+[A-Za-z]+\.[a-z]+
print(s10) // \\[A-Z]+[A-Za-z]+\.[a-z]+

枚举

Swift的枚举有类的一些特性,比如计算属性、实例方法、扩展、遵循协议等等。

enum E1:String, CaseIterable {
    case e1, e2 = "12"
// 关联值
enum E2 {
    case e1([String])
    case e2(Int)
let e1 = E2.e1(["one","two"])
let e2 = E2.e2(3)
switch e1 {
case .e1(let array):
    print(array)
case .e2(let int):
    print(int)
print(e2)
// 原始值
print(E1.e1.rawValue)
// 遵循 CaseIterable 协议可迭代
for ie in E1.allCases {
    print("show \(ie)")
// 递归枚举
enum RE {
    case v(String)
    indirect case node(l:RE, r:RE)
let lNode = RE.v("left")
let rNode = RE.v("right")
let pNode = RE.node(l: lNode, r: rNode)
switch pNode {
case .v(let string):
    print(string)
case .node(let l, let r):
    print(l,r)
    switch l {
    case .v(let string):
        print(string)
    case .node(let l, let r):
        print(l, r)
    switch r {
    case .v(let string):
        print(string)
    case .node(let l, let r):
        print(l, r)
}

@unknown 用来区分固定的枚举和可能改变的枚举的能力。@unknown 用于防止未来新增枚举属性会进行提醒提示完善每个 case 的处理。

// @unknown
enum E3 {
    case e1, e2, e3
func fe1(e: E3) {
    switch e {
    case .e1:
        print("e1 ok")
    case .e2:
        print("e2 ok")
    case .e3:
        print("e3 ok")
    @unknown default:
        print("not ok")
}

符合 Comparable 协议的枚举可以进行比较。

// Comparable 枚举比较
enum E4: Comparable {
    case e1, e2
    case e3(i: Int)
    case e4
let e3 = E4.e4
let e4 = E4.e3(i: 3)
let e5 = E4.e3(i: 2)
let e6 = E4.e1
print(e3 > e4) // true
let a1 = [e3, e4, e5, e6]
let a2 = a1.sorted()
for i in a2 {
    print(i.self)
/// e1
/// e3(i: 2)
/// e3(i: 3)
/// e4

泛型

泛型可以减少重复代码,是一种抽象的表达方式。where 关键字可以对泛型做约束。

func fn<T>(p: T) -> [T] {
    var r = [T]()
    r.append(p)
    return r
print(fn(p: "one"))
// 结构体
struct S1<T> {
    var arr = [T]()
    mutating func add(_ p: T) {
        arr.append(p)
var s1 = S1(arr: ["zero"])
s1.add("one")
s1.add("two")
print(s1.arr) // ["zero", "one", "two"]

关联类型

protocol pc {
    associatedtype T
    mutating func add(_ p: T)
struct S2: pc {
    typealias T = String // 类型推导,可省略
    var strs = [String]()
    mutating func add(_ p: String) {
        strs.append(p)
}

泛型适用于嵌套类型

struct S3<T> {
    struct S4 {
        var p: T
    var p1: T
    var p2: S4
let s2 = S3(p1: 1, p2: S3.S4(p: 3))
let s3 = S3(p1: "one", p2: S3.S4(p: "three"))
print(s2,s3)

不透明类型

不透明类型会隐藏类型,让使用者更关注功能。不透明类型和协议很类似,不同的是不透明比协议限定的要多,协议能够对应更多类型。

protocol P {
    func f() -> String
struct S1: P {
    func f() -> String {
        return "one\n"
struct S2<T: P>: P {
    var p: T
    func f() -> String {
        return p.f() + "two\n"
struct S3<T1: P, T2: P>: P {
    var p1: T1
    var p2: T2
    func f() -> String {
        return p1.f() + p2.f() + "three\n"
func someP() -> some P {
    return S3(p1: S1(), p2: S2(p: S1()))
let r = someP()
print(r.f())

函数调用者决定返回什么类型是泛型,函数自身决定返回什么类型使用不透明返回类型。

Result

Result 类型用来处理错误,特别适用异步接口的错误处理。

extension URLSession {
    func dataTaskWithResult(
        with url: URL,
        handler: @escaping (Result<Data, Error>) -> Void
    ) -> URLSessionDataTask {
        dataTask(with: url) { data, _, err in
            if let err = err {
                handler(.failure(err))
            } else {
                handler(.success(data ?? Data()))
let url = URL(string: "https://ming1016.github.io/")!
// 以前网络请求
let t1 = URLSession.shared.dataTask(with: url) {
    data, _, error in
    if let err = error {
        print(err)
    } else if let data = data {
        print(String(decoding: data, as: UTF8.self))
t1.resume()
// 使用 Result 网络请求
let t2 = URLSession.shared.dataTaskWithResult(with: url) { result in
    switch result {
    case .success(let data):
        print(String(decoding: data, as: UTF8.self))
    case .failure(let err):
        print(err)
t2.resume()

类型转换

使用 is 关键字进行类型判断, 使用as 关键字来转换成子类。

class S0 {}
class S1: S0 {}
class S2: S0 {}
var a = [S0]()
a.append(S1())
a.append(S2())
for e in a {
    // 类型判断
    if e is S1 {
        print("Type is S1")
    } else if e is S2 {
        print("Type is S2")
    // 使用 as 关键字转换成子类
    if let s1 = e as? S1 {
        print("As S1 \(s1)")
    } else if let s2 = e as? S2 {
        print("As S2 \(s2)")
}

类和结构体

类可以定义属性、方法、构造器、下标操作。类使用扩展来扩展功能,遵循协议。类还以继承,运行时检查实例类型。

class C {
     var p: String
     init(_ p: String) {
         self.p = p
     // 下标操作
     subscript(s: String) -> String {
         get {
             return p + s
         set {
             p = s + newValue
 let c = C("hi")
 print(c.p)
 print(c[" ming"])
 c["k"] = "v"
 print(c.p)

结构体

结构体是值类型,可以定义属性、方法、构造器、下标操作。结构体使用扩展来扩展功能,遵循协议。

struct S {
    var p1: String = ""
    var p2: Int
extension S {
    func f() -> String {
        return p1 + String(p2)
var s = S(p2: 1)
s.p1 = "1"
print(s.f()) // 11

属性

类、结构体或枚举里的变量常量就是他们的属性。

struct S {
    static let sp = "类型属性" // 类型属性通过类型本身访问,非实例访问
    var p1: String = ""
    var p2: Int = 1
    // cp 是计算属性
    var cp: Int {
        get {
            return p2 * 2
        set {
            p2 = newValue + 2
    // 只有 getter 的是只读计算属性
    var rcp: Int {
        p2 * 4
print(S.sp)
print(S().cp) // 2
var s = S()
s.cp = 3
print(s.p2) // 5
print(S().rcp) // 4

willSet 和 didSet 是属性观察器,可以在属性值设置前后插入自己的逻辑处理。

键路径表达式作为函数

struct S2 {
    let p1: String
    let p2: Int
let s2 = S2(p1: "one", p2: 1)
let s3 = S2(p1: "two", p2: 2)
let a1 = [s2, s3]
let a2 = a1.map(\.p1)
print(a2) // ["one", "two"]

方法

enum E: String {
    case one, two, three
    func showRawValue() {
        print(rawValue)
let e = E.three
e.showRawValue() // three
// 可变的实例方法,使用 mutating 标记
struct S {
    var p: String
    mutating func addFullStopForP() {
        p += "."
var s = S(p: "hi")
s.addFullStopForP()
print(s.p)
// 类方法
class C {
    class func cf() {
        print("类方法")
}

static和class关键字修饰的方法类似 OC 的类方法。static 可以修饰存储属性,而 class 不能;class 修饰的方法可以继承,而 static 不能。在协议中需用 static 来修饰。

静态下标方法

// 静态下标
struct S2 {
    static var sp = [String: Int]()
    static subscript(_ s: String, d: Int = 10) -> Int {
        get {
            return sp[s] ?? d
        set {
            sp[s] = newValue
S2["key1"] = 1
S2["key2"] = 2
print(S2["key2"]) // 2
print(S2["key3"]) // 10

自定义类型中实现了 callAsFunction() 的话,该类型的值就可以直接调用。

// callAsFunction()
struct S3 {
    var p1: String
    func callAsFunction() -> String {
        return "show \(p1)"
let s2 = S3(p1: "hi")
print(s2()) // show hi

继承

类能继承另一个类,继承它的方法、属性等。

// 类继承
class C1 {
    var p1: String
    var cp1: String {
        get {
            return p1 + " like ATM"
        set {
            p1 = p1 + newValue
    init(p1: String) {
        self.p1 = p1
    func sayHi() {
        print("Hi! \(p1)")
class C2: C1 {
    var p2: String
    init(p2: String) {
        self.p2 = p2
        super.init(p1: p2 + "'s father")
C2(p2: "Lemon").sayHi() // Hi! Lemon's father
// 重写父类方法
class C3: C2 {
    override func sayHi() {
        print("Hi! \(p2)")
C3(p2: "Lemon").sayHi() // Hi! Lemon
// 重写计算属性
class C4: C1 {
    override var cp1: String {
        get {
            return p1 + " like Out of the blade"
        set {
            p1 = p1 + newValue
print(C1(p1: "Lemon").cp1) // Lemon like ATM
print(C4(p1: "Lemon").cp1) // Lemon like Out of the blade

通过 final 关键字可以防止类被继承,final 还可以用于属性和方法。使用 super 关键字指代父类。

函数式

map

map 可以依次处理数组中元素,并返回一个处理后的新数组。

let a1 = ["a", "b", "c"]
let a2 = a1.map {
    "\($0)2"
print(a2) // ["a2", "b2", "c2"]

使用 compactMap 可以过滤 nil 的元素。flatMap 会将多个数组合成一个数组返回。

filter

根据指定条件返回

let a1 = ["a", "b", "c", "call my name"]
let a2 = a1.filter {
    $0.prefix(1) == "c"
print(a2) // ["c", "call my name"]

reduce

reduce 可以将迭代中返回的结果用于下个迭代中,并,还能让你设个初始值。

let a1 = ["a", "b", "c", "call my name.", "get it?"]
let a2 = a1.reduce("Hey u,", { partialResult, s in
    // partialResult 是前面返回的值,s 是遍历到当前的值
    partialResult + " \(s)"
print(a2) // Hey u, a b c call my name. get it?

sorted

排序

// 类型遵循 Comparable
let a1 = ["a", "b", "c", "call my name.", "get it?"]
let a2 = a1.sorted()
let a3 = a1.sorted(by: >)
let a4 = a1.sorted(by: <)
print(a2) // Hey u, a b c call my name. get it?
print(a3) // ["get it?", "call my name.", "c", "b", "a"]
print(a4) // ["a", "b", "c", "call my name.", "get it?"]
// 类型不遵循 Comparable
struct S {
    var s: String
    var i: Int
let a5 = [S(s: "a", i: 0), S(s: "b", i: 1), S(s: "c", i: 2)]
let a6 = a5
    .sorted { l, r in
        l.i > r.i
    .map {
print(a6) // [2, 1, 0]

控制流

If • If let • If case let

// if
let s = "hi"
if s.isEmpty {
    print("String is Empty")
} else {
    print("String is \(s)")
// 三元条件
s.isEmpty ? print("String is Empty again") : print("String is \(s) again")
// if let-else
func f(s: String?) {
    if let s1 = s {
        print("s1 is \(s1)")
    } else {
        print("s1 is nothing")
    // nil-coalescing
    let s2 = s ?? "nothing"
    print("s2 is \(s2)")
f(s: "something")
f(s: nil)
// if case let
enum E {
    case c1(String)
    case c2([String])
    func des() {
        switch self {
        case .c1(let string):
            print(string)
        case .c2(let array):
            print(array)
E.c1("enum c1").des()
E.c2(["one", "two", "three"]).des()

Guard guard, guard let

更好地处理异常情况

// guard
func f1(p: String) -> String {
    guard p.isEmpty != true else {
        return "Empty string."
    return "String \(p) is not empty."
print(f1(p: "")) // Empty string.
print(f1(p: "lemon")) // String lemon is not empty.
// guard let
func f2(p1: String?) -> String {
    guard let p2 = p1 else {
        return "Nil."
    return "String \(p2) is not nil."
print(f2(p1: nil)) // Nil.
print(f2(p1: "lemon")) // String lemon is not nil.

遍历 For-in

let a = ["one", "two", "three"]
for str in a {
    print(str)
// 使用下标范围
for i in 0..<10 {
    print(i)
// 使用 enumerated
for (i, str) in a.enumerated() {
    print("第\(i + 1)个是:\(str)")
// for in where
for str in a where str.prefix(1) == "t" {
    print(str)
// 字典 for in,遍历是无序的
let dic = [
    "one": 1,
    "two": 2,
    "three": 3
for (k, v) in dic {
    print("key is \(k), value is \(v)")
// stride
for i in stride(from: 10, through: 0, by: -2) {
    print(i)
 */

While while, repeat-while

// while
var i1 = 10
while i1 > 0 {
    print("positive even number \(i1)")
    i1 -= 2
// repeat while
var i2 = 10
repeat {
    print("positive even number \(i2)")
    i2 -= 2
} while i2 > 0

使用 break 结束遍历,使用 continue 跳过当前作用域,继续下个循环

Switch

func f1(pa: String, t:(String, Int)) {
    var p1 = 0
    var p2 = 10
    switch pa {
    case "one":
        p1 = 1
    case "two":
        p1 = 2
        fallthrough // 继续到下个 case 中
    default:
        p2 = 0
    print("p1 is \(p1)")
    print("p2 is \(p2)")
    // 元组
    switch t {
    case ("0", 0):
        print("zero")
    case ("1", 1):
        print("one")
    default:
        print("no")
f1(pa: "two", t:("1", 1))
 p1 is 2
 p2 is 0
// 枚举
enum E {
    case one, two, three, unknown(String)
func f2(pa: E) {
    var p: String
    switch pa {
    case .one:
        p = "1"
    case .two:
        p = "2"
    case .three:
        p = "3"
    case let .unknown(u) where Int(u) ?? 0 > 0 : // 枚举关联值,使用 where 增加条件
        p = u
    case .unknown(_):
        p = "negative number"
    print(p)
f2(pa: E.one) // 1
f2(pa: E.unknown("10")) // 10
f2(pa: E.unknown("-10")) // negative number

集合

数组 [1, 2, 3]

数组是有序集合

var a0: [Int] = [1, 10]
a0.append(2)
a0.remove(at: 0)
print(a0) // [10, 2]
let a1 = ["one", "two", "three"]
let a2 = ["three", "four"]
// 找两个集合的不同
let dif = a1.difference(from: a2) // swift的 diffing 算法在这 http://www.xmailserver.org/diff2.pdf swift实现在  swift/stdlib/public/core/Diffing.swift
for c in dif {
    switch c {
    case .remove(let o, let e, let a):
        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")
    case .insert(let o, let e, let a):
        print("offset:\(o), element:\(e), associatedWith:\(String(describing: a))")
 remove offset:1, element:four, associatedWith:nil
 insert offset:0, element:one, associatedWith:nil
 insert offset:1, element:two, associatedWith:nil
let a3 = a2.applying(dif) ?? [] // 可以用于添加删除动画
print(a3) // ["one", "two", "three"]

dif 有第三个 case 值 .insert(let offset, let element, let associatedWith) 可以跟踪成对的变化,用于高级动画。

从数组中随机取一个元素

print(a0.randomElement() ?? 0)

数组排序

// 排序
struct S1 {
    let n: Int
    var b = true
let a4 = [
    S1(n: 1),
    S1(n: 10),
    S1(n: 3),
    S1(n: 2)
let a5 = a4.sorted { i1, i2 in
    i1.n < i2.n
for n in a5 {
    print(n)
/// S1(n: 1)
/// S1(n: 2)
/// S1(n: 3)
/// S1(n: 10)
let a6 = [1,10,4,7,2]
print(a6.sorted(by: >)) // [10, 7, 4, 2, 1]

可以加到数组扩展中,通过扩展约束能够指定特定元素类型的排序,代码如下:

extension Array where Element == Int {
    // 升序
    func intSortedASC() -> [Int] {
        return self.sorted(by: <)
    // 降序
    func intSortedDESC() -> [Int] {
        return self.sorted(by: <)
print(a6.intSortedASC()) // 使用扩展增加自定义排序能力

在数组中检索满足条件的元素,代码如下:

// 第一个满足条件了就返回
let a7 = a4.first {
    $0.n == 10
print(a7?.n ?? 0)
// 是否都满足了条件
print(a4.allSatisfy { $0.n == 1 }) // false
print(a4.allSatisfy(\.b)) // true
// 找出最大的那个
print(a4.max(by: { e1, e2 in
    e1.n < e2.n
}) ?? S1(n: 0))
// S1(n: 10, b: true)
// 看看是否包含某个元素
print(a4.contains(where: {
    $0.n == 7
// false

一些切割数组的方法。

// 切片
// 取前3个,并不是直接复制,对于大的数组有性能优势。
print(a6[..<3]) // [1, 10, 4] 需要做越界检查
print(a6.prefix(30)) // [1, 10, 4, 7, 2] 不需要做越界检查,也是切片,性能一样
// 去掉前3个
print(a6.dropFirst(3)) // [7, 2]

prefix(while:) 和 drop(while:) 方法,顺序遍历执行闭包里的逻辑判断,满足条件就返回,遇到不匹配就会停止遍历。prefix 返回满足条件的元素集合,drop 返回停止遍历之后那些元素集合。

let a8 = [8, 9, 20, 1, 35, 3]
let a9 = a8.prefix {
    $0 < 30
print(a9) // [8, 9, 20, 1]
let a10 = a8.drop {
    $0 < 30
print(a10) // [35, 3]

比 filter 更高效的删除元素的方法 removeAll

// 删除所有不满足条件的元素
var a11 = [1, 3, 5, 12, 25]
a11.removeAll { $0 < 10 }
print(a11) // [4, 3, 1, 3, 3] 随机
// 创建未初始化的数组
let a12 = (0...4).map { _ in
    Int.random(in: 0...5)
print(a12) // [0, 3, 3, 2, 5] 随机

#if 用于后缀表达式

// #if 用于后缀表达式
let a13 = a11
#if os(iOS)
    .count
#else
    .reduce(0, +)
#endif
print(a13) //37

Sets Set

Set 是无序集合,元素唯一

let s0: Set<Int> = [2, 4]
let s1: Set = [2, 10, 6, 4, 8]
let s2: Set = [7, 3, 5, 1, 9, 10]
let s3 = s1.union(s2) // 合集
let s4 = s1.intersection(s2) // 交集
let s5 = s1.subtracting(s2) // 非交集部分
let s6 = s1.symmetricDifference(s2) // 非交集的合集
print(s3) // [4, 2, 1, 7, 3, 10, 8, 9, 6, 5]
print(s4) // [10]
print(s5) // [8, 4, 2, 6]
print(s6) // [9, 1, 3, 4, 5, 2, 6, 8, 7]
// s0 是否被 s1 包含
print(s0.isSubset(of: s1)) // true
// s1 是否包含了 s0
print(s1.isSuperset(of: s0)) // true
let s7: Set = [3, 5]
// s0 和 s7 是否有交集
print(s0.isDisjoint(with: s7)) // true
// 可变 Set
var s8: Set = ["one", "two"]
s8.insert("three")
s8.remove("one")
print(s8) // ["two", "three"]

字典 [:]

字典是无序集合,键值对应。

var d1 = [
    "k1": "v1",
    "k2": "v2"
d1["k3"] = "v3"
d1["k4"] = nil
print(d1) // ["k2": "v2", "k3": "v3", "k1": "v1"]
for (k, v) in d1 {
    print("key is \(k), value is \(v)")
 key is k1, value is v1
 key is k2, value is v2
 key is k3, value is v3
if d1.isEmpty == false {
    print(d1.count) // 3
// mapValues
let d2 = d1.mapValues {
    $0 + "_new"
print(d2) // ["k2": "v2_new", "k3": "v3_new", "k1": "v1_new"]
// 对字典的值或键进行分组
let d3 = Dictionary(grouping: d1.values) {
    $0.count
print(d3) // [2: ["v1", "v2", "v3"]]
// 从字典中取值,如果键对应无值,则使用通过 default 指定的默认值
d1["k5", default: "whatever"] += "."
print(d1["k5"] ?? "") // whatever.
let v1 = d1["k3", default: "whatever"]
print(v1) // v3
// compactMapValues() 对字典值进行转换和解包。可以解可选类型,并去掉 nil 值
let d4 = [
    "k1": 1,
    "k2": 2,
    "k3": nil
let d5 = d4.mapValues { $0 }
let d6 = d4.compactMapValues{ $0 }
print(d5)
// ["k3": nil, "k1": Optional(1), "k2": Optional(2)]
print(d6)
// ["k1": 1, "k2": 2]

操作符

赋值 =, +=. -=, *=, /=

let i1 = 1
var i2 = i1
i2 = 2
print(i2) // 2
i2 += 1
print(i2) // 3
i2 -= 2
print(i2) // 1
i2 *= 10
print(i2) // 10
i2 /= 2
print(i2) // 5

计算符 +, -, *, /, %

let i1 = 1
let i2 = i1
print((i1 + i2 - 1) * 10 / 2 % 3) // 2
print("i" + "1") // i1
// 一元运算符
print(-i1) // -1

比较运算符 ==, >

遵循 Equatable 协议可以使用 == 和 != 来判断是否相等

print(1 > 2) // false
struct S: Equatable {
    var p1: String
    var p2: Int
let s1 = S(p1: "one", p2: 1)
let s2 = S(p1: "two", p2: 2)
let s3 = S(p1: "one", p2: 2)
let s4 = S(p1: "one", p2: 1)
print(s1 == s2) // false
print(s1 == s3) // false
print(s1 == s4) // true

类需要实现 == 函数

class C: Equatable {
    var p1: String
    var p2: Int
    init(p1: String, p2: Int) {
        self.p1 = p1
        self.p2 = p2
    static func == (l: C, r: C) -> Bool {
        return l.p1 == r.p1 && l.p2 == r.p2
let c1 = C(p1: "one", p2: 1)
let c2 = C(p1: "one", p2: 1)
print(c1 == c2)
// 元组比较
// 会先比较第一个数,第一个无法比较才会比较第二个数
// 字符串比较和字母大小还有长度有关。先比较字母大小,在比较长度
("apple", 1) < ("apple", 2) // true
("applf", 1) < ("apple", 2) // false
("appl", 2) < ("apple", 1) // true
("appm", 2) < ("apple", 1) // false

三元 _ ? _ : _

简化 if else 写法

// if else
func f1(p: Int) {
    if p > 0 {
        print("positive number")
    } else {
        print("negative number")
// 三元
func f2(p: Int) {
    p > 0 ? print("positive number") : print("negative number")
f1(p: 1)
f2(p: 1)

Nil-coalescing ??

简化 if let else 写法

// if else
func f1(p: Int?) {
    if let i = p {
        print("p have value is \(i)")
    } else {
        print("p is nil, use defalut value")
// 使用 ??
func f2(p: Int?) {
    let i = p ?? 0
    print("p is \(i)")
}

范围 a…b

简化的值范围表达方式。

// 封闭范围
for i in 0...10 {
    print(i)
// 半开范围
for i in 0..<10 {
    print(i)
// 单侧区间
let nums = [5,6,7,8]
print(nums[2...]) // 7 8

逻辑 !, &&, ||

let i1 = -1
let i2 = 2
if i1 != i2 && (i1 < 0 || i2 < 0) {
    print("i1 and i2 not equal, and one of them is negative number.")
}

恒等 ===, !==

恒等返回是否引用了相同实例。

class C {
    var p: String
    init(p: String) {
        self.p = p
let c1 = C(p: "one")
let c2 = C(p: "one")
let c3 = c1
print(c1 === c2) // false
print(c1 === c3) // true
print(c1 !== c2) // true

运算符

位运算符

let i1: UInt8 = 0b00001111
let i2 = ~i1 // Bitwise NOT Operator(按位取反运算符),取反
let i3: UInt8 = 0b00111111
let i4 = i1 & i3 // Bitwise AND Operator(按位与运算符),都为1才是1
let i5 = i1 | i3 // Bitwise OR Operator(按位或运算符),有一个1就是1
let i6 = i1 ^ i3 // Bitwise XOR Operator(按位异或运算符),不同为1,相同为0
print(i1,i2,i3,i4,i5,i6)
// << 按位左移,>> 按位右移
let i7 = i1 << 1
let i8 = i1 >> 2
print(i7,i8)

溢出运算符,有 &+、&- 和 &*

var i1 = Int.max
print(i1) // 9223372036854775807
i1 = i1 &+ 1
print(i1) // -9223372036854775808
i1 = i1 &+ 10
print(i1) // -9223372036854775798
var i2 = UInt.max
i2 = i2 &+ 1
print(i2) // 0

运算符函数包括前缀运算符、后缀运算符、复合赋值运算符以及等价运算符。另,还可以自定义运算符,新的运算符要用 operator 关键字进行定义,同时要指定 prefix、infix 或者 postfix 修饰符。

基础库

时间

Date 的基本用法如下:

let now = Date()
// Date 转 时间戳
let interval = now.timeIntervalSince1970 // 时间戳
let df = DateFormatter()
df.dateFormat = "yyyy 年 MM 月 dd 日 HH:mm:ss"
print("时间戳:\(Int(interval))") // 时间戳:1642399901
print("格式化的时间:" + df.string(from: now)) // 格式化的时间:2022 年 01 月 17 日 14:11:41
df.dateStyle = .short
print("short 样式时间:" + df.string(from: now)) // short 样式时间:2022/1/17
df.locale = Locale(identifier: "zh_Hans_CN")
df.dateStyle = .full
print("full 样式时间:" + df.string(from: now)) // full 样式时间:2022年1月17日 星期一
// 时间戳转 Date
let date = Date(timeIntervalSince1970: interval)
print(date) // 2022-01-17 06:11:41 +0000

复杂的时间操作,比如说 GitHub 接口使用的是 ISO 标准,RSS 输出的是 RSS 标准字符串,不同标准对应不同时区的时间计算处理,可以使用开源库 SwiftDate 来完成。示例代码如下:

import SwiftDate
// 使用 SwiftDate 库
let cn = Region(zone: Zones.asiaShanghai, locale: Locales.chineseChina)
SwiftDate.defaultRegion = cn
print("2008-02-14 23:12:14".toDate()?.year ?? "") // 2008
let d1 = "2022-01-17T23:20:35".toISODate(region: cn)
guard let d1 = d1 else {
    return
print(d1.minute) // 20
let d2 = d1 + 1.minutes
print(d2.minute)
// 两个 DateInRegion 相差时间 interval
let i1 = DateInRegion(Date(), region: cn) - d1
let s1 = i1.toString {
    $0.maximumUnitCount = 4
    $0.allowedUnits = [.day, .hour, .minute]
    $0.collapsesLargestUnit = true
    $0.unitsStyle = .abbreviated
    $0.locale = Locales.chineseChina
print(s1) // 9小时45分钟

格式化

使用标准库的格式来描述不同场景的情况可以不用去考虑由于不同地区的区别,这些在标准库里就可以自动完成了。

描述两个时间之间相差多长时间

// 计算两个时间之间相差多少时间,支持多种语言字符串
let d1 = Date().timeIntervalSince1970 - 60 * 60 * 24
let f1 = RelativeDateTimeFormatter()
f1.dateTimeStyle = .named
f1.formattingContext = .beginningOfSentence
f1.locale = Locale(identifier: "zh_Hans_CN")
let str = f1.localizedString(for: Date(timeIntervalSince1970: d1), relativeTo: Date())
print(str) // 昨天
// 简写
let str2 = Date.now.addingTimeInterval(-(60 * 60 * 24))
    .formatted(.relative(presentation: .named))
print(str2) // yesterday

描述多个事物

// 描述多个事物
let s1 = ListFormatter.localizedString(byJoining: ["冬天","春天","夏天","秋天"])
print(s1)

描述名字

// 名字
let f2 = PersonNameComponentsFormatter()
var nc1 = PersonNameComponents()
nc1.familyName = "戴"
nc1.givenName = "铭"
nc1.nickname = "铭哥"
print(f2.string(from: nc1)) // 戴铭
f2.style = .short
print(f2.string(from: nc1)) // 铭哥
f2.style = .abbreviated
print(f2.string(from: nc1)) // 戴
var nc2 = PersonNameComponents()
nc2.familyName = "Dai"
nc2.givenName = "Ming"
nc2.nickname = "Starming"
f2.style = .default
print(f2.string(from: nc2)) // Ming Dai
f2.style = .short
print(f2.string(from: nc2)) // Starming
f2.style = .abbreviated
print(f2.string(from: nc2)) // MD
// 取出名
let componets = f2.personNameComponents(from: "戴铭")
print(componets?.givenName ?? "") // 铭

描述数字

// 数字
let f3 = NumberFormatter()
f3.locale = Locale(identifier: "zh_Hans_CN")
f3.numberStyle = .currency
print(f3.string(from: 123456) ?? "") // ¥123,456.00
f3.numberStyle = .percent
print(f3.string(from: 123456) ?? "") // 12,345,600%
let n1 = 1.23456
let n1Str = n1.formatted(.number.precision(.fractionLength(3)).rounded())
print(n1Str) // 1.235

描述地址

// 地址
import Contacts
let f4 = CNPostalAddressFormatter()
let address = CNMutablePostalAddress()
address.street = "海淀区王庄路XX号院X号楼X门XXX"
address.postalCode = "100083"
address.city = "北京"
address.country = "中国"
print(f4.string(from: address))
/// 海淀区王庄路XX号院X号楼X门XXX
/// 北京 100083
/// 中国

度量值

标准库里的物理量,在这个文档里有详细列出,包括角度、平方米等。

// 参考:https://developer.apple.com/documentation/foundation/nsdimension
let m1 = Measurement(value: 1, unit: UnitLength.kilometers)
let m2 = m1.converted(to: .meters) // 千米转米
print(m2) // 1000.0 m
// 度量值转为本地化的值
let mf = MeasurementFormatter()
mf.locale = Locale(identifier: "zh_Hans_CN")
print(mf.string(from: m1)) // 1公里

一些物理公式供参考:

面积 = 长度 × 长度
体积 = 长度 × 长度 × 长度 = 面积 × 长度
速度=长度/时间
加速度=速度/时间
力 = 质量 × 加速度
扭矩 = 力 × 长度
压力 = 力 / 面积
密度=质量 / 体积
能量 = 功率 × 时间
电阻 = 电压 / 电流

Data

数据压缩和解压

// 对数据的压缩
let d1 = "看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?看看能够压缩多少?".data(using: .utf8)! as NSData
print("ori \(d1.count) bytes")
    /// 压缩算法
    /// * lz4
    /// * lzma
    /// * zlib
    /// * lzfse
    let compressed = try d1.compressed(using: .zlib)
    print("comp \(compressed.count) bytes")
    // 对数据解压
    let decomressed = try compressed.decompressed(using: .zlib)
    let deStr = String(data: decomressed as Data, encoding: .utf8)
    print(deStr ?? "")
} catch {}
/// ori 297 bytes
/// comp 37 bytes

文件

文件的一些基本操作的代码如下:

let path1 = "/Users/mingdai/Downloads/1.html"
let path2 = "/Users/mingdai/Documents/GitHub/"
let u1 = URL(string: path1)
    // 写入
    let url1 = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: u1, create: true) // 保证原子性安全保存
    print(url1)
    // 读取
    let s1 = try String(contentsOfFile: path1, encoding: .utf8)
    print(s1)
} catch {}
// 检查路径是否可用
let u2 = URL(fileURLWithPath:path2)
    let values = try u2.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
    if let capacity = values.volumeAvailableCapacityForImportantUsage {
        print("可用: \(capacity)")
    } else {
        print("不可用")
} catch {
    print("错误: \(error.localizedDescription)")
}

怎么遍历多级目录结构中的文件呢?看下面的代码的实现:

// 遍历路径下所有目录
let u3 = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
let fm = FileManager.default
fm.enumerator(atPath: u3.path)?.forEach({ path in
    guard let path = path as? String else {
        return
    let url = URL(fileURLWithPath: path, relativeTo: u3)
    print(url.lastPathComponent)
})

可以使用 FileWrapper 来创建文件夹和文件。举个例子:

// FileWrapper 的使用
// 创建文件
let f1 = FileWrapper(regularFileWithContents: Data("# 第 n 个文件\n ## 标题".utf8))
f1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()
f1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()
// 创建文件夹
let folder1 = FileWrapper(directoryWithFileWrappers: [
    "file1.md": f1
folder1.fileAttributes[FileAttributeKey.creationDate.rawValue] = Date()
folder1.fileAttributes[FileAttributeKey.modificationDate.rawValue] = Date()
    try folder1.write(
        to: URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("NewFolder"),
        options: .atomic,
        originalContentsURL: nil
} catch {}
print(FileManager.default.currentDirectoryPath)

上面代码写起来比较繁琐,对 FileWrapper 更好的封装可以参考这篇文章《 A Type-Safe FileWrapper | Heberti Almeida 》。

文件读写处理完整能力可以参看这个库 GitHub - JohnSundell/Files: A nicer way to handle files & folders in Swift

本地或者网络上,比如网盘和FTP的文件发生变化时,怎样知道能够观察到呢?

通过 HTTPHeader 里的 If-Modified-Since、Last-Modified、If-None-Match 和 Etag 等字段来判断文件的变化,本地则是使用 DispatchSource.makeFileSystemObjectSource 来进行的文件变化监听。可以参考 KZFileWatchers 库的做法。

Scanner

let s1 = """
one1,
two2,
three3.
let sn1 = Scanner(string: s1)
while !sn1.isAtEnd {
    if let r1 = sn1.scanUpToCharacters(from: .newlines) {
        print(r1 as String)
/// one1,
/// two2,
/// three3.
// 找出数字
let sn2 = Scanner(string: s1)
sn2.charactersToBeSkipped = CharacterSet.decimalDigits.inverted // 不是数字的就跳过
var p: Int = 0
while !sn2.isAtEnd {
    if sn2.scanInt(&p) {
        print(p)
/// 1
/// 2
/// 3

上面的代码还不是那么 Swifty,可以通过用AnySequence和AnyIterator来包装下,将序列中的元素推迟到实际需要时再来处理,这样性能也会更好些。具体实现可以参看《 String parsing in Swift 》这篇文章。

AttributeString

效果如下:

代码如下:

var aStrs = [AttributedString]()
var aStr1 = AttributedString("""
正文内容,具体查看链接。
这里摘出第一个重点,还要强调的内容。
// 标题
let title = aStr1.range(of: "标题")
guard let title = title else {
    return aStrs
var c1 = AttributeContainer() // 可复用容器
c1.inlinePresentationIntent = .stronglyEmphasized
c1.font = .largeTitle
aStr1[title].setAttributes(c1)
// 链接
let link = aStr1.range(of: "链接")
guard let link = link else {
    return aStrs
var c2 = AttributeContainer() // 链接
c2.strokeColor = .blue
c2.link = URL(string: "https://ming1016.github.io/")
aStr1[link].setAttributes(c2.merging(c1)) // 合并 AttributeContainer
// Runs
let i1 = aStr1.range(of: "重点")
let i2 = aStr1.range(of: "强调")
guard let i1 = i1, let i2 = i2 else {
    return aStrs
var c3 = AttributeContainer()
c3.foregroundColor = .yellow
c3.inlinePresentationIntent = .stronglyEmphasized
aStr1[i1].setAttributes(c3)
aStr1[i2].setAttributes(c3)
for r in aStr1.runs {
    print("-------------")
    print(r.attributes)
aStrs.append(aStr1)
// Markdown
    let aStr2 = try AttributedString(markdown: """
    内容[链接](https://ming1016.github.io/)。需要**强调**的内容。
    aStrs.append(aStr2)
} catch {}

SwiftUI 的 Text 可以直接读取 AttributedString 来进行显示。

随机

用法:

let ri = Int.random(in: 0..<10)
print(ri) // 0到10随机数
let a = [0, 1, 2, 3, 4, 5]
print(a.randomElement() ?? 0) // 数组中随机取个数
print(a.shuffled()) // 随机打乱数组顺序

UserDefaults

使用方法如下:

enum UDKey {
    static let k1 = "token"
let ud = UserDefaults.standard
ud.set("xxxxxx", forKey: UDKey.k1)
let tk = ud.string(forKey: UDKey.k1)
print(tk ?? "")

模式

单例

struct S {
    static let shared = S()
    private init() {
        // 防止实例初始化
}

系统及设备

系统判断

#if os(tvOS)
     // do something in tvOS
#elseif os(iOS)
     // do somthing in iOS
#elseif os(macOS)
    // do somthing in macOS
#endif

版本兼容

// 版本
@available(iOS 15, *)
func f() {
// 版本检查
if #available(iOS 15, macOS 12, *) {
} else {
    // nothing happen
}

canImport 判断库是否可使用

#if canImport(SpriteKit)
   // iOS 等苹果系统执行
#else
   // 非苹果系统
#endif

targetEnvironment 环境的判断

#if targetEnvironment(simulator)
   // 模拟器
#else
   // 真机
#endif

自带属性包装

@resultBuilder

结果生成器(Result builders),通过传递序列创建新值,SwiftUI就是使用的结果生成器将多个视图生成一个视图

@resultBuilder
struct RBS {
    // 基本闭包支持
    static func buildBlock(_ components: Int...) -> Int {
        components.reduce(0) { partialResult, i in
            partialResult + i
    // 支持条件判断
    static func buildEither(first component: Int) -> Int {
        component
    static func buildEither(second component: Int) -> Int {
        component
    // 支持循环
    static func buildArray(_ components: [Int]) -> Int {
        components.reduce(0) { partialResult, i in
            partialResult + i
let a = RBS.buildBlock(
print(a) // 6
// 应用到函数中
@RBS func f1() -> Int {
print(f1()) // 6
// 设置了 buildEither 就可以在闭包中进行条件判断。
@RBS func f2(stopAtThree: Bool) -> Int {
    if stopAtThree == true {
    } else {
print(f2(stopAtThree: false)) // 21
// 设置了 buildArray 就可以在闭包内使用循环了
@RBS func f3() -> Int {
    for i in 1...3 {
        i * 2
print(f3()) // 12

@dynamicMemberLookup 动态成员查询

@dynamicMemberLookup 指示访问属性时调用一个已实现的处理动态查找的下标方法 subscript(dynamicMemeber:),通过指定属性字符串名返回值。使用方法如下:

@dynamicMemberLookup
struct D {
    // 找字符串
    subscript(dynamicMember m: String) -> String {
        let p = ["one": "first", "two": "second"]
        return p[m, default: ""]
    // 找整型
    subscript(dynamicMember m: String) -> Int {
        let p = ["one": 1, "two": 2]
        return p[m, default: 0]
    // 找闭包
    subscript(dynamicMember m: String) -> (_ s: String) -> Void {
        return {
            print("show \($0)")
    // 静态数组成员
    var p = ["This is a member"]
    // 动态数组成员
    subscript(dynamicMember m: String) -> [String] {
        return ["This is a dynamic member"]
let d = D()
let s1: String = d.one
print(s1) // first
let i1: Int = d.one
print(i1) // 1
d.show("something") // show something
print(d.p) // ["This is a member"]
let dynamicP:[String] = d.dp
print(dynamicP) // ["This is a dynamic member"]

类使用 @dynamicMemberLookup,继承的类也会自动加上 @dynamicMemberLookup。协议上定义 @dynamicMemberLookup,通过扩展可以默认实现 subscript(dynamicMember:) 方法。

@dynamicCallable 动态可调用类型

@dynamicCallable 动态可调用类型。通过实现 dynamicallyCall 方法来定义变参的处理。

@dynamicCallable
struct D {
    // 带参数说明
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
        let firstArg = args.first?.value ?? 0
        return firstArg * 2
    // 无参数说明
    func dynamicallyCall(withArguments args: [String]) -> String {
        var firstArg = ""
        if args.count > 0 {
            firstArg = args[0]
        return "show \(firstArg)"
let d = D()
let i = d(numberIs: 2)
print(i) // 4
let s = d("hi")
print(s) // show hi

自带协议

Hashable

struct H: Hashable {
    var p1: String
    var p2: Int
    // 提供随机 seed
    func hash(into hasher: inout Hasher) {
        hasher.combine(p1)
let h1 = H(p1: "one", p2: 1)
let h2 = H(p1: "two", p2: 2)
var hs1 = Hasher()
hs1.combine(h1)
hs1.combine(h2)
print(h1.hashValue) // 7417088153212460033 随机值
print(h2.hashValue) // -6972912482785541972 随机值
print(hs1.finalize()) // 7955861102637572758 随机值
print(h1.hashValue) // 7417088153212460033 和前面 h1 一样
let h3 = H(p1: "one", p2: 1)
print(h3.hashValue) // 7417088153212460033 和前面 h1 一样
var hs2 = Hasher()
hs2.combine(h3)
hs2.combine(h2)
print(hs2.finalize()) // 7955861102637572758 和前面 hs1 一样

应用生命周期内,调用 combine() 添加相同属性哈希值相同,由于 Hasher 每次都会使用随机的 seed,因此不同应用生命周期,也就是下次启动的哈希值,就会和上次的哈希值不同。

Codable

JSON 没有 id 字段

如果SwiftUI要求数据Model都是遵循Identifiable协议的,而有的json没有id这个字段,可以使用扩展struct的方式解决:

struct CommitModel: Decodable, Hashable {
  var sha: String
  var author: AuthorModel
  var commit: CommitModel
extension CommitModel: Identifiable {
  var id: String {
    return sha
}

网络

网络状态检查

通过 Network 库的 NWPathMonitor 来检查

import Combine
import Network
// 网络状态检查 network state check
final class Nsck: ObservableObject {
    static let shared = Nsck()
    private(set) lazy var pb = mkpb()
    @Published private(set) var pt: NWPath
    private let monitor: NWPathMonitor
    private lazy var sj = CurrentValueSubject<NWPath, Never>(monitor.currentPath)
    private var sb: AnyCancellable?
    init() {
        monitor = NWPathMonitor()
        pt = monitor.currentPath
        monitor.pathUpdateHandler = { [weak self] path in
            self?.pt = path
            self?.sj.send(path)
        monitor.start(queue: DispatchQueue.global())
    deinit {
        monitor.cancel()
        sj.send(completion: .finished)
    private func mkpb() -> AnyPublisher<NWPath, Never> {
        return sj.eraseToAnyPublisher()
}

使用方法

var sb = Set<AnyCancellable>()
var alertMsg = ""
Nsck.shared.pb
    .sink { _ in
    } receiveValue: { path in
        alertMsg = path.debugDescription
        switch path.status {
        case .satisfied:
            alertMsg = ""
        case .unsatisfied:
            alertMsg = " "
        case .requiresConnection:
            alertMsg = " "
        @unknown default:
            alertMsg = " "
        if path.status == .unsatisfied {
            switch path.unsatisfiedReason {
            case .notAvailable:
                alertMsg += "网络不可用"
            case .cellularDenied:
                alertMsg += "蜂窝网不可用"
            case .wifiDenied:
                alertMsg += "Wifi不可用"
            case .localNetworkDenied:
                alertMsg += "网线不可用"
            @unknown default:
                alertMsg += "网络不可用"
    .store(in: &sb)

动画

布局动画

import SwiftUI
struct AnimateLayout: View {
 @State var changeLayout: Bool = true
 @Namespace var namespace
 var body: some View {
  VStack(spacing: 30) {
   if changeLayout {
    HStack { items }
   } else {
    VStack { items }
   Button("切换布局") {
    withAnimation { changeLayout.toggle() }
  .padding()
 @ViewBuilder var items: some View {
  Text("one")
   .matchedGeometryEffect(id: "one", in: namespace)
  Text("Two")
   .matchedGeometryEffect(id: "Two", in: namespace)
  Text("Three")
   .matchedGeometryEffect(id: "Three", in: namespace)
}

安全

Keychain

使用方法:

let d1 = Data("keyChain github token".utf8)
let service = "access-token"
let account = "github"
let q1 = [
    kSecValueData: d1,
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: service,
    kSecAttrAccount: account
] as CFDictionary
// 添加一个 keychain
let status = SecItemAdd(q1, nil)
// 如果已经添加过会抛出 -25299 错误代码,需要调用 SecItemUpdate 来进行更新
if status == errSecDuplicateItem {
    let q2 = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account
    ] as CFDictionary
    let q3 = [
        kSecValueData: d1
    ] as CFDictionary
    SecItemUpdate(q2, q3)
// 读取
let q4 = [
    kSecAttrService: service,
    kSecAttrAccount: account,
    kSecClass: kSecClassGenericPassword,
    kSecReturnData: true
] as CFDictionary
var re: AnyObject?
SecItemCopyMatching(q4, &re)
guard let reData = re as? Data else { return }
print(String(decoding: reData, as: UTF8.self)) // keyChain github token
// 删除
let q5 = [
    kSecAttrService: service,
    kSecAttrAccount: account,
    kSecClass: kSecClassGenericPassword,
] as CFDictionary
SecItemDelete(q5)

工程

程序入口点

Swift 允许全局编写 Swift 代码,实际上 clang 会自动将代码包进一个模拟 C 的函数中。Swift 也能够指定入口点,比如 @UIApplicationMain 或 @NSApplicationMain,UIKit 启动后生命周期管理是 AppDelegate 和 SceneDelegate,《 Understanding the iOS 13 Scene Delegate 》这篇有详细介绍。

@UIApplicationMain 和 @NSApplicationMain 会自动生成入口点。这些入口点都是平台相关的,Swift 发展来看是多平台的,这样在 Swift 5.3 时引入了 @main,可以方便的指定入口点。代码如下:

@main // 要定义个静态的 main 函数
struct M {
  static func main() {
    print("let's begin")
}

ArgumentParser 库,Swift 官方开源的一个开发命令行工具的库,也支持 @main。使用方法如下:

import ArgumentParser
@main
struct C: ParsableCommand {
  @Argument(help: "Start")
  var phrase: String
  func run() throws {
    for _ in 1...5 {
      print(phrase)
}

专题

Swift 那些事

Swift 各版本演进

Swift 1.1

  • countElements() 改成了 count()。
  • @NSApplicationMain 可以在 macOS 上使用。

Swift 1.2

  • 引入 Set 类型。
  • if let 可以放到一起,使用逗号分隔。
  • 新增 zip() 和 flatMap()。
  • 类增加静态方法和静态属性,使用 static 关键字描述。
  • as! 用于类型强转,失败会崩溃。
  • @noescape 用于描述作为参数闭包,用来告诉 Swift 闭包将在函数返回前使用。
  • 常量可以延后初始化。

Swift 2.0

  • 增加 guard 关键字,用于解可选项值。
  • defer 关键字用来延迟执行,即使抛出错误了都会在最后执行。
  • ErrorType 协议,以及 throws、do、try 和 catch 的引入用来处理错误。
  • characters 加上 count,用来替代 count()。
  • #available 用来检查系统版本。

Swift 2.1

  • 字符串插值可以包含字符串字面符号。

Swift 2.2

  • FILE , LINE FUNCTION 换成 #file,#line 和 #function。
  • 废弃 ++ 和 -- 操作符。
  • C 语言风格 for 循环废弃。
  • 废弃变量参数,因为变量参数容易和 inout 搞混。
  • 废弃字符串化的选择器,选择器不再能写成字符串了。
  • 元组可直接比较是否相等。

Swift 3.0

  • 规范动词和名词来命名。
  • 去掉 NS 前缀。
  • 方法名描述参数部分变为参数名。
  • 省略没必要的单词,命名做了简化呢。比如 stringByTrimmingCharactersInSet 就换成了 trimmingCharacters。
  • 枚举的属性使用小写开头。
  • 引入 C 函数的属性。

Swift 3.1

  • 序列新增 prefix(while:) 和 drop(while:) 方法,顺序遍历执行闭包里的逻辑判断,满足条件就返回,遇到不匹配就会停止遍历。prefix 返回满足条件的元素集合,drop 返回停止遍历之后那些元素集合。
  • 泛型适用于嵌套类型。
  • 类型的扩展可以使用约束条件,比如扩展数组时,加上元素为整数的约束,这样的扩展就只会对元素为整数的数组有效。

Swift 4.0

Swift 4.1

Swift 4.2

Swift 5.0

Swift 5.1

Swift 5.2

Swift 5.3

Swift 5.4

Swift 5.5

规范

注意事项

参考:

多用静态特性。swift 在编译期间所做的优化比 OC 要多,这是由于他的静态派发、泛型特化、写时复制这些静态特性决定的。另外通过 final 和 private 这样的表示可将动态特性转化为静态方式,编译开启 WMO 可以自动推导出哪些动态派发可转化为静态派发。

如何避免崩溃?

  • 字典:用结构体替代
  • Any:可用泛型或关联关联类型替代
  • as? :少用 AnyObject,多用泛型或不透明类型
  • !:要少用

好的实践?

  • 少用继承,多用 protocol
  • 多用 extension 对自己代码进行管理

资料推荐

书单

  • 《Thinking in SwiftUI》
  • 《Swift 进阶》
  • 《函数式Swift》
  • 《深入解析Mac OS X & iOS操作系统》
  • 《LLVM Techniques, Tips, and Best Practices Clang and Middle-End Libraries》
  • 《Learn LLVM 12》
  • 《Crafting Interpreters》
  • 《TCP/IP Illustrated》
  • 《松本行弘的程序世界》
  • 《现代操作系统》
  • 《深入理解计算机系统》
  • 《程序员的自我修养》
  • 《Head First 设计模式》

三方库使用

SQLite.swift 的使用

下面是 SQLite.swift 库的使用介绍,包括了数据库创建,表创建,表的添加、更新、删除、查找等处理方法

import SQLite
struct DB {
    static let shared = DB()
    static let path = NSSearchPathForDirectoriesInDomains(
        .applicationSupportDirectory, .userDomainMask, true
    ).first!
    let BBDB: Connection?
    private init() {
            print(DB.path)
            BBDB = try Connection("\(DB.path)/github.sqlite3")
        } catch {
            BBDB = nil
        /// Swift 类型和 SQLite 类型对标如下:
        /// Int64 = INTEGER
        /// Double = REAL
        /// String = TEXT
        /// nil = NULL
        /// SQLite.Blob = BLOB
    // 创建表
    func cTbs() throws {
            try ReposNotiDataHelper.createTable()
            try DevsNotiDataHelper.createTable()
        } catch {
            throw DBError.connectionErr
enum DBError: Error {
    case connectionErr, insertErr, deleteErr, searchErr, updateErr, nilInData
protocol DataHelperProtocol {
    associatedtype T
    static func createTable() throws -> Void
    static func insert(i: T) throws -> Int64
    static func delete(i: T) throws -> Void
    static func findAll() throws -> [T]?
// MARK: 开发者更新提醒
typealias DBDevNoti = (
    login: String,
    lastReadId: String,
    unRead: Int
struct DevsNotiDataHelper: DataHelperProtocol {
    static let table = Table("devsNoti")
    static let login = Expression<String>("login")
    static let lastReadId = Expression<String>("lastReadId")
    static let unRead = Expression<Int>("unRead")
    typealias T = DBDevNoti
    static func createTable() throws {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
            let _ = try db.run(table.create(ifNotExists: true) { t in
                t.column(login, unique: true)
                t.column(lastReadId, defaultValue: "")
                t.column(unRead, defaultValue: 0)
        } catch _ {
            throw DBError.connectionErr
    } // end createTable
    static func insert(i: DBDevNoti) throws -> Int64 {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
        let insert = table.insert(login <- i.login, lastReadId <- i.lastReadId, unRead <- i.unRead)
            let rowId = try db.run(insert)
            guard rowId > 0 else {
                throw DBError.insertErr
            return rowId
        } catch {
            throw DBError.insertErr
    } // end insert
    static func delete(i: DBDevNoti) throws {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
        let query = table.filter(login == i.login)
            let tmp = try db.run(query.delete())
            guard tmp == 1 else {
                throw DBError.deleteErr
        } catch {
            throw DBError.deleteErr
    } // end delete
    static func find(sLogin: String) throws -> DBDevNoti? {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
        let query = table.filter(login == sLogin)
        let items = try db.prepare(query)
        for i in items {
            return DBDevNoti(login: i[login], lastReadId: i[lastReadId], unRead: i[unRead])
        return nil
    } // end find
    static func update(i: DBDevNoti) throws {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
        let query = table.filter(login == i.login)
            if try db.run(query.update(lastReadId <- i.lastReadId, unRead <- i.unRead)) > 0 {
            } else {
                throw DBError.updateErr
        } catch {
            throw DBError.updateErr
    } // end update
    static func findAll() throws -> [DBDevNoti]? {
        guard let db = DB.shared.BBDB else {
            throw DBError.connectionErr
        var arr = [DBDevNoti]()
        let items = try db.prepare(table)
        for i in items {
            arr.append(DBDevNoti(login: i[login], lastReadId: i[lastReadId], unRead: i[unRead]))
        return arr
    } // end find all
}

使用时,可以在初始化时这么做:

// MARK: 初始化数据库
et db = DB.shared
    try db.cTbs() // 创建表
} catch {
}

使用的操作示例如下:

do {
    if let fd = try ReposNotiDataHelper.find(sFullName: r.id) {
        reposDic[fd.fullName] = fd.unRead
    } else {
            let _ = try ReposNotiDataHelper.insert(i: DBRepoNoti(fullName: r.id, lastReadCommitSha: "", unRead: 0))
            reposDic[r.id] = 0
        } catch {
            return reposDic
} catch {
    return reposDic
}

macOS

范例

三栏结构

三栏结构架子搭建,代码如下:

import SwiftUI
struct SwiftPamphletApp: View {
    var body: some View {
        NavigationView {
            SPSidebar()
            Text("第二栏")
            Text("第三栏")
        .navigationTitle("Swift 小册子")
        .toolbar {
            ToolbarItem(placement: ToolbarItemPlacement.navigation) {
                Button {
                    NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
                } label: {
                    Label("Sidebar", systemImage: "sidebar.left")
struct SPSidebar: View {
    var body: some View {
        List {
            Section("第一组") {
                NavigationLink("第一项", destination: SPList(title: "列表1"))
                    .badge(3)
                NavigationLink("第二项", destination: SPList(title: "列表2"))
            Section("第二组") {
                NavigationLink("第三项", destination: SPList(title: "列表3"))
                NavigationLink("第四项", destination: SPList(title: "列表4"))
        .listStyle(SidebarListStyle())
        .frame(minWidth: 160)
        .toolbar {
            ToolbarItem {
                Menu {
                    Text("1")
                    Text("2")
                } label: {
                    Label("Label", systemImage: "slider.horizontal.3")
struct SPList: View {
    var title: String
    @State var searchText: String = ""
    var body: some View {
        List(0..<3) { i in
            Text("内容\(i)")
        .toolbar(content: {
            Button {
            } label: {
                Label("Add", systemImage: "plus")
        .navigationTitle(title)
        .navigationSubtitle("副标题")
        .searchable(text: $searchText)
}

显示效果如下:


共享菜单

struct ShareView: View {
    var s: String
    var body: some View {
        Menu {
            Button {
                let p = NSPasteboard.general
                p.declareTypes([.string], owner: nil)
                p.setString(s, forType: .string)
            } label: {
                Text("拷贝链接")
                Image(systemName: "doc.on.doc")
            Divider()
            ForEach(NSSharingService.sharingServices(forItems: [""]), id: \.title) { item in
                Button {
                    item.perform(withItems: [s])
                } label: {
                    Text(item.title)
                    Image(nsImage: item.image)
        } label: {
            Text("分享")
            Image(systemName: "square.and.arrow.up")
}

剪贴板

添加和读取剪贴板的方法如下:

// 读取剪贴板内容
let s = NSPasteboard.general.string(forType: .string)
guard let s = s else {
    return
print(s)
// 设置剪贴板内容
let p = NSPasteboard.general
p.declareTypes([.string], owner: nil)
p.setString(s, forType: .string)

Combine

介绍

Combine 是什么?

WWDC 2019苹果推出Combine,Combine是一种响应式编程范式,采用声明式的Swift API。

Combine 写代码的思路是你写代码不同于以往命令式的描述如何处理数据,Combine 是要去描述好数据会经过哪些逻辑运算处理。这样代码更好维护,可以有效的减少嵌套闭包以及分散的回调等使得代码维护麻烦的苦恼。

声明式和过程时区别可见如下代码:

// 所有数相加
// 命令式思维
func sum1(arr: [Int]) -> Int {
  var sum: Int = 0
  for v in arr {
    sum += v
  return sum
// 声明式思维
func sum2(arr: [Int]) -> Int {
  return arr.reduce(0, +)
}

Combine 主要用来处理异步的事件和值。苹果 UI 框架都是在主线程上进行 UI 更新,Combine 通过 Publisher 的 receive 设置回主线程更新UI会非常的简单。

已有的 RxSwift 和 ReactiveSwift 框架和 Combine 的思路和用法类似。

Combine 的三个核心概念

  • 发布者
  • 订阅者
  • 操作符

简单举个发布数据和类属性绑定的例子:

let pA = Just(0)
let _ = pA.sink { v in
    print("pA is: \(v)")
let pB = [7,90,16,11].publisher
let _ = pB
    .sink { v in
        print("pB: \(v)")
class AClass {
    var p: Int = 0 {
        didSet {
            print("property update to \(p)")
let o = AClass()
let _ = pB.assign(to: \.p, on: o)

Combine 资料

官方文档链接 Combine | Apple Developer Documentation 。还有 Using Combine 这里有大量使用示例,内容较全。官方讨论Combine的论坛 Topics tagged combine 。StackOverflow上相关问题 Newest ‘combine’ Questions

WWDC上关于Combine的Session如下:

和Combine相关的Session:

使用说明

publisher

publisher 是发布者,sink 是订阅者

import Combine
var cc = Set<AnyCancellable>()
struct S {
    let p1: String
    let p2: String
[S(p1: "1", p2: "one"), S(p1: "2", p2: "two")]
    .publisher
    .print("array")
    .sink {
        print($0)
    .store(in: &cc)

输出

array: receive subscription: ([戴铭的Swift小册子.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "1", p2: "one"), 戴铭的Swift小册子.AppDelegate.(unknown context at $10ac82d20).(unknown context at $10ac82da4).S(p1: "2", p2: "two")])
array: request unlimited
array: receive value: (S(p1: "1", p2: "one"))
S(p1: "1", p2: "one")
array: receive value: (S(p1: "2", p2: "two"))
S(p1: "2", p2: "two")
array: receive finished

Just

Just 是发布者,发布的数据在初始化时完成

import Combine
var cc = Set<AnyCancellable>()
struct S {
    let p1: String
    let p2: String
let pb = Just(S(p1: "1", p2: "one"))
    .print("pb")
    .sink {
        print($0)
    .store(in: &cc)

输出

pb: receive subscription: (Just)
pb: request unlimited
pb: receive value: (S(p1: "1", p2: "one"))
S(p1: "1", p2: "one")
pb: receive finished

PassthroughSubject

PassthroughSubject 可以传递多值,订阅者可以是一个也可以是多个,send 指明 completion 后,订阅者就没法接收到新发送的值了。

import Combine
var cc = Set<AnyCancellable>()
struct S {
    let p1: String
    let p2: String
enum CError: Error {
    case aE, bE
let ps1 = PassthroughSubject<S, CError>()
    .print("ps1")
    .sink { c in
        print("completion:", c) // send 了 .finished 后会执行
    } receiveValue: { s in
        print("receive:", s)
    .store(in: &cc)
ps1.send(S(p1: "1", p2: "one"))
ps1.send(completion: .failure(CError.aE)) // 和 .finished 一样后面就不会发送了
ps1.send(S(p1: "2", p2: "two"))
ps1.send(completion: .finished)
ps1.send(S(p1: "3", p2: "three"))
// 多个订阅者
let ps2 = PassthroughSubject<String, Never>()
ps2.send("one") // 订阅之前 send 的数据没有订阅者可以接收
ps2.send("two")
let sb1 = ps2
    .print("ps2 sb1")
    .sink { s in
    print(s)
ps2.send("three") // 这个 send 的值会被 sb1
let sb2 = ps2
    .print("ps2 sb2")
    .sink { s in
        print(s)
ps2.send("four") // 这个 send 的值会被 sb1 和 sb2 接受
sb1.store(in: &cc)
sb2.store(in: &cc)
ps2.send(completion: .finished)

输出

ps1: receive subscription: (PassthroughSubject)
ps1: request unlimited
ps1: receive value: (S(p1: "1", p2: "one"))
receive: S(p1: "1", p2: "one")
ps1: receive error: (aE)
completion: failure(戴铭的Swift小册子.AppDelegate.(unknown context at $10b15ce10).(unknown context at $10b15cf3c).CError.aE)
ps2 sb1: receive subscription: (PassthroughSubject)
ps2 sb1: request unlimited
ps2 sb1: receive value: (three)
three
ps2 sb2: receive subscription: (PassthroughSubject)
ps2 sb2: request unlimited
ps2 sb1: receive value: (four)
ps2 sb2: receive value: (four)
ps2 sb1: receive finished
ps2 sb2: receive finished

Empty

import Combine
var cc = Set<AnyCancellable>()
struct S {
    let p1: String
    let p2: String
let ept = Empty<S, Never>() // 加上 completeImmediately: false 后面即使用 replaceEmpty 也不会接受值
    .print("ept")
    .sink { c in
        print("completion:", c)
    } receiveValue: { s in
        print("receive:", s)
    .store(in: &cc)
ept.replaceEmpty(with: S(p1: "1", p2: "one"))
    .sink { c in
        print("completion:", c)
    } receiveValue: { s in
        print("receive:", s)
    .store(in: &cc)

输出

ept: receive subscription: (Empty)
ept: request unlimited
ept: receive finished
completion: finished
receive: S(p1: "1", p2: "one")
completion: finished

CurrentValueSubject

CurrentValueSubject 的订阅者可以收到订阅时已发出的那条数据

import Combine
var cc = Set<AnyCancellable>()
let cs = CurrentValueSubject<String, Never>("one")
cs.send("two")
cs.send("three")
let sb1 = cs
    .print("cs sb1")
    .sink {
        print($0)
cs.send("four")
cs.send("five")
let sb2 = cs
    .print("cs sb2")
    .sink {
        print($0)
cs.send("six")
sb1.store(in: &cc)
sb2.store(in: &cc)

输出

cs sb1: receive subscription: (CurrentValueSubject)
cs sb1: request unlimited
cs sb1: receive value: (three)
three
cs sb1: receive value: (four)
cs sb1: receive value: (five)
cs sb2: receive subscription: (CurrentValueSubject)
cs sb2: request unlimited
cs sb2: receive value: (five)
cs sb1: receive value: (six)
cs sb2: receive value: (six)
cs sb1: receive cancel
cs sb2: receive cancel

removeDuplicates

使用 removeDuplicates,重复的值就不会发送了。

import Combine
var cc = Set<AnyCancellable>()
let pb = ["one","two","three","three","four"]
    .publisher
let sb = pb
    .print("sb")
    .removeDuplicates()
    .sink {
        print($0)
sb.store(in: &cc)

输出

sb: receive subscription: (["one", "two", "three", "three", "four"])
sb: request unlimited
sb: receive value: (one)
sb: receive value: (two)
sb: receive value: (three)
three
sb: receive value: (three)
sb: request max: (1) (synchronous)
sb: receive value: (four)
sb: receive finished

flatMap

flatMap 能将多个发布者的值打平发送给订阅者

import Combine
var cc = Set<AnyCancellable>()
struct S {
    let p: AnyPublisher<String, Never>
let s1 = S(p: Just("one").eraseToAnyPublisher())
let s2 = S(p: Just("two").eraseToAnyPublisher())
let s3 = S(p: Just("three").eraseToAnyPublisher())
let pb = [s1, s2, s3].publisher
let sb = pb
    .print("sb")
    .flatMap {
    .sink {
        print($0)
sb.store(in: &cc)

输出

sb: receive subscription: ([戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])
sb: request unlimited
sb: receive value: (S(p: AnyPublisher))
sb: receive value: (S(p: AnyPublisher))
sb: receive value: (S(p: AnyPublisher))
three
sb: receive finished

append

append 会在发布者发布结束后追加发送数据,发布者不结束,append 的数据不会发送。

import Combine
var cc = Set<AnyCancellable>()
let pb = PassthroughSubject<String, Never>()
let sb = pb
    .print("sb")
    .append("five", "six")
    .sink {
        print($0)
sb.store(in: &cc)
pb.send("one")
pb.send("two")
pb.send("three")
pb.send(completion: .finished)

输出

sb: receive subscription: ([戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher), 戴铭的Swift小册子.AppDelegate.(unknown context at $101167070).(unknown context at $1011670f4).S(p: AnyPublisher)])
sb: request unlimited
sb: receive value: (S(p: AnyPublisher))
sb: receive value: (S(p: AnyPublisher))
sb: receive value: (S(p: AnyPublisher))
three
sb: receive finished

prepend

prepend 会在发布者发布前先发送数据,发布者不结束也不会受影响。发布者和集合也可以被打平发布。

import Combine
var cc = Set<AnyCancellable>()
let pb1 = PassthroughSubject<String, Never>()
let pb2 = ["nine", "ten"].publisher
let sb = pb1
    .print("sb")
    .prepend(pb2)
    .prepend(["seven","eight"])
    .prepend("five", "six")
    .sink {
        print($0)
sb.store(in: &cc)
pb1.send("one")
pb1.send("two")
pb1.send("three")

输出

five
seven
eight
sb: receive subscription: (PassthroughSubject)
sb: request unlimited
sb: receive value: (one)
sb: receive value: (two)
sb: receive value: (three)
three
sb: receive cancel

merge

订阅者可以通过 merge 合并多个发布者发布的数据

import Combine
var cc = Set<AnyCancellable>()
let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()
let sb1 = ps1.merge(with: ps2)
    .sink {
        print($0)
ps1.send("one")
ps1.send("two")
ps2.send("1")
ps2.send("2")
ps1.send("three")
sb1.store(in: &cc)

输出

sb1: receive subscription: (Merge)
sb1: request unlimited
sb1: receive value: (one)
sb1: receive value: (two)
sb1: receive value: (1)
sb1: receive value: (2)
sb1: receive value: (three)
three
sb1: receive cancel

zip

zip 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会组合成一个数据给订阅者。

import Combine
var cc = Set<AnyCancellable>()
let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()
let ps3 = PassthroughSubject<String, Never>()
let sb1 = ps1.zip(ps2, ps3)
    .print("sb1")
    .sink {
        print($0)
ps1.send("one")
ps1.send("two")
ps1.send("three")
ps2.send("1")
ps2.send("2")
ps1.send("four")
ps2.send("3")
ps3.send("一")
sb1.store(in: &cc)

输出

sb1: receive subscription: (Zip)
sb1: request unlimited
sb1: receive value: (("one", "1", "一"))
("one", "1", "一")
sb1: receive cancel

combineLatest

combineLatest 会合并多个发布者发布的数据,只有当多个发布者都发布了数据后才会触发合并,合并每个发布者发布的最后一个数据。

import Combine
var cc = Set<AnyCancellable>()
let ps1 = PassthroughSubject<String, Never>()
let ps2 = PassthroughSubject<String, Never>()
let ps3 = PassthroughSubject<String, Never>()
let sb1 = ps1.combineLatest(ps2, ps3)
    .print("sb1")
    .sink {
        print($0)
ps1.send("one")
ps1.send("two")
ps1.send("three")
ps2.send("1")
ps2.send("2")
ps1.send("four")
ps2.send("3")
ps3.send("一")
ps3.send("二")
sb1.store(in: &cc)

输出

sb1: receive subscription: (CombineLatest)
sb1: request unlimited
sb1: receive value: (("four", "3", "一"))
("four", "3", "一")
sb1: receive value: (("four", "3", "二"))
("four", "3", "二")
sb1: receive cancel

Scheduler

Scheduler 处理队列。

import Combine
var cc = Set<AnyCancellable>()
let sb1 = ["one","two","three"].publisher
    .print("sb1")
    .subscribe(on: DispatchQueue.global())
    .handleEvents(receiveOutput: {
        print("receiveOutput",$0)
    .receive(on: DispatchQueue.main)
    .sink {
        print($0)
sb1.store(in: &cc)

输出

sb1: receive subscription: ([1, 2, 3])
sb1: request unlimited
sb1: receive value: (1)
receiveOutput 1
sb1: receive value: (2)
receiveOutput 2
sb1: receive value: (3)
receiveOutput 3
sb1: receive finished
3

使用场景

网络请求

网络URLSession.dataTaskPublisher使用例子如下:

let req = URLRequest(url: URL(string: "http://www.starming.com")!)
let dpPublisher = URLSession.shared.dataTaskPublisher(for: req)

一个请求Github接口并展示结果的例子

//
// CombineSearchAPI.swift
// SwiftOnly (iOS)
// Created by Ming Dai on 2021/11/4.
import SwiftUI
import Combine
struct CombineSearchAPI: View {
  var body: some View {
    GithubSearchView()
// MARK: Github View
struct GithubSearchView: View {
  @State var str: String = "Swift"
  @StateObject var ss: SearchStore = SearchStore()
  @State var repos: [GithubRepo] = []
  var body: some View {
    NavigationView {
      List {
        TextField("输入:", text: $str, onCommit: fetch)
        ForEach(self.ss.repos) { repo -> GithubRepoCell in
          GithubRepoCell(repo: repo)
      .navigationTitle("搜索")
    .onAppear(perform: fetch)
  private func fetch() {
    self.ss.search(str: self.str)
struct GithubRepoCell: View {
  let repo: GithubRepo
  var body: some View {
    VStack(alignment: .leading, spacing: 20) {
      Text(self.repo.name)
      Text(self.repo.description)
// MARK: Github Service
struct GithubRepo: Decodable, Identifiable {
  let id: Int
  let name: String
  let description: String
struct GithubResp: Decodable {
  let items: [GithubRepo]
final class GithubSearchManager {
  func search(str: String) -> AnyPublisher<GithubResp, Never> {
    guard var urlComponents = URLComponents(string: "https://api.github.com/search/repositories") else {
      preconditionFailure("链接无效")
    urlComponents.queryItems = [URLQueryItem(name: "q", value: str)]
    guard let url = urlComponents.url else {
      preconditionFailure("链接无效")
    let sch = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
    return URLSession.shared
      .dataTaskPublisher(for: url)
      .receive(on: sch)
      .tryMap({ element -> Data in
        print(String(decoding: element.data, as: UTF8.self))
        return element.data
      .decode(type: GithubResp.self, decoder: JSONDecoder())
      .catch { _ in
        Empty().eraseToAnyPublisher()
      .eraseToAnyPublisher()
final class SearchStore: ObservableObject {
  @Published var query: String = ""
  @Published var repos: [GithubRepo] = []
  private let searchManager: GithubSearchManager
  private var cancellable = Set<AnyCancellable>()
  init(searchManager: GithubSearchManager = GithubSearchManager()) {
    self.searchManager = searchManager
    $query
      .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
      .flatMap { query -> AnyPublisher<[GithubRepo], Never> in
        return searchManager.search(str: query)
          .map {
            $0.items
          .eraseToAnyPublisher()
      .receive(on: DispatchQueue.main)
      .assign(to: \.repos, on: self)
      .store(in: &cancellable)
  func search(str: String) {
    self.query = str
}

抽象基础网络能力,方便扩展,代码如下:

//
// CombineAPI.swift
// SwiftOnly (iOS)
// Created by Ming Dai on 2021/11/4.
import SwiftUI
import Combine
struct CombineAPI: View {
  var body: some View {
    RepListView(vm: .init())
struct RepListView: View {
  @ObservedObject var vm: RepListVM
  var body: some View {
    NavigationView {
      List(vm.repos) { rep in
        RepListCell(rep: rep)
      .alert(isPresented: $vm.isErrorShow) { () -> Alert in
        Alert(title: Text("出错了"), message: Text(vm.errorMessage))
      .navigationBarTitle(Text("仓库"))
    .onAppear {
      vm.apply(.onAppear)
struct RepListCell: View {
  @State var rep: RepoModel
  var body: some View {
    HStack() {
      VStack() {
        AsyncImage(url: URL(string: rep.owner.avatarUrl ?? ""), content: { image in
          image
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 100, height: 100)
        placeholder: {
          ProgressView()
            .frame(width: 100, height: 100)
        Text("\(rep.owner.login)")
          .font(.system(size: 10))
      VStack(alignment: .leading, spacing: 10) {
        Text("\(rep.name)")
          .font(.title)
        Text("\(rep.stargazersCount)")
          .font(.title3)
        Text("\(String(describing: rep.description ?? ""))")
        Text("\(String(describing: rep.language ?? ""))")
          .font(.title3)
      .font(.system(size: 14))
// MARK: Repo View Model
final class RepListVM: ObservableObject, UnidirectionalDataFlowType {
  typealias InputType = Input
  private var cancellables: [AnyCancellable] = []
  // Input
  enum Input {
    case onAppear
  func apply(_ input: Input) {
    switch input {
    case .onAppear:
      onAppearSubject.send(())
  private let onAppearSubject = PassthroughSubject<Void, Never>()
  // Output
  @Published private(set) var repos: [RepoModel] = []
  @Published var isErrorShow = false
  @Published var errorMessage = ""
  @Published private(set) var shouldShowIcon = false
  private let resSubject = PassthroughSubject<SearchRepoModel, Never>()
  private let errSubject = PassthroughSubject<APISevError, Never>()
  private let apiSev: APISev
  init(apiSev: APISev = APISev()) {
    self.apiSev = apiSev
    bindInputs()
    bindOutputs()
  private func bindInputs() {
    let req = SearchRepoRequest()
    let resPublisher = onAppearSubject
      .flatMap { [apiSev] in
        apiSev.response(from: req)
          .catch { [weak self] error -> Empty<SearchRepoModel, Never> in
            self?.errSubject.send(error)
            return .init()
    let resStream = resPublisher
      .share()
      .subscribe(resSubject)
    // 其它异步事件,比如日志等操作都可以做成Stream加到下面数组内。
    cancellables += [resStream]
  private func bindOutputs() {
    let repStream = resSubject
      .map {
        $0.items
      .assign(to: \.repos, on: self)
    let errMsgStream = errSubject
      .map { error -> String in
        switch error {
        case .resError: return "network error"
        case .parseError: return "parse error"
      .assign(to: \.errorMessage, on: self)
    let errStream = errSubject
      .map { _ in
      .assign(to: \.isErrorShow, on: self)
    cancellables += [repStream,errStream,errMsgStream]
protocol UnidirectionalDataFlowType {
  associatedtype InputType
  func apply(_ input: InputType)
// MARK: Repo Request and Models
struct SearchRepoRequest: APIReqType {
  typealias Res = SearchRepoModel
  var path: String {
    return "/search/repositories"
  var qItems: [URLQueryItem]? {
    return [
      .init(name: "q", value: "Combine"),
      .init(name: "order", value: "desc")
struct SearchRepoModel: Decodable {
  var items: [RepoModel]
struct RepoModel: Decodable, Hashable, Identifiable {
  var id: Int64
  var name: String
  var fullName: String
  var description: String?
  var stargazersCount: Int = 0
  var language: String?
  var owner: OwnerModel
struct OwnerModel: Decodable, Hashable, Identifiable {
  var id: Int64
  var login: String
  var avatarUrl: String?
// MARK: API Request Fundation
protocol APIReqType {
  associatedtype Res: Decodable
  var path: String { get }
  var qItems: [URLQueryItem]? { get }
protocol APISevType {
  func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request: APIReqType
final class APISev: APISevType {
  private let rootUrl: URL
  init(rootUrl: URL = URL(string: "https://api.github.com")!) {
    self.rootUrl = rootUrl
  func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request : APIReqType {
    let path = URL(string: req.path, relativeTo: rootUrl)!
    var comp = URLComponents(url: path, resolvingAgainstBaseURL: true)!
    comp.queryItems = req.qItems
    print(comp.url?.description ?? "url wrong")
    var req = URLRequest(url: comp.url!)
    req.addValue("application/json", forHTTPHeaderField: "Content-Type")
    let de = JSONDecoder()
    de.keyDecodingStrategy = .convertFromSnakeCase
    return URLSession.shared.dataTaskPublisher(for: req)
      .map { data, res in
        print(String(decoding: data, as: UTF8.self))
        return data
      .mapError { _ in
        APISevError.resError
      .decode(type: Request.Res.self, decoder: de)
      .mapError(APISevError.parseError)
      .receive(on: RunLoop.main)
      .eraseToAnyPublisher()
enum APISevError: Error {
  case resError
  case parseError(Error)
}

KVO

例子如下:

private final class KVOObject: NSObject {
  @objc dynamic var intV: Int = 0
  @objc dynamic var boolV: Bool = false
let o = KVOObject()
let _ = o.publisher(for: \.intV)
  .sink { v in
    print("value : \(v)")
  }

通知

使用例子如下:

extension Notification.Name {
    static let noti = Notification.Name("nameofnoti")
let notiPb = NotificationCenter.default.publisher(for: .noti, object: nil)
        .sink {
            print($0)
        }

退到后台接受通知的例子如下:

class A {
  var storage = Set<AnyCancellable>()
  init() {
    NotificationCenter.default.publisher(for: UIWindowScene.didEnterBackgroundNotification)
      .sink { _ in
        print("enter background")
      .store(in: &self.storage)