戴銘的 Swift 小冊子

update:內容已更新到 6.0 版本,程序已上架 macOS 蘋果商店匈庭,點擊安裝 方便獲取應用的更新秉撇。使用 WWDC23 技術重構甜攀。6.0版本介紹見戴銘的開發(fā)小冊子6.0

背景說明

越來越多同學打算開始用 Swift 來開發(fā)了琐馆,可很多人以前都沒接觸過 Swift规阀。這篇和我以前文章不同的是,本篇只是面向 Swift 零基礎的同學瘦麸,內容主要是一些直接可用的小例子谁撼,例子可以直接在工程中用或自己調試著看。

記得以前 PHP 有個 chm 的手冊瞎暑,寫的很簡單彤敛,但很全,每個知識點都有例子了赌,社區(qū)版每個知識點下面還有留言互動。因此玄糟,我弄了個 Swift 的手冊勿她,是個 macOS 程序。建議使用我開發(fā)的這個 macOS 程序來瀏覽阵翎。

截圖如下:








這個程序是Swift寫的逢并,按照聲明式UI,響應式編程范式開發(fā)的郭卫,源碼也可以看看砍聊。與其講一堆,不如調著試贰军。

下面是文本內容玻蝌。注:代碼中簡化變量名是為了能更快速關注到語言用法。

語法

基礎

變量 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)是小檸檬的生日")

注釋 //

// 單行注釋
/*
多行注釋第一行瓦糟。
多行注釋第二行赊窥。
*/ 
// MARK: 會在minimap上展示
// TODO: 待做
// FIXME: 待修復

可選 ?, !

可能會是 nil 的變量就是可選變量。當變量為 nil 通過??操作符可以提供一個默認值狸页。

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

閉包

閉包也可以叫做 lambda锨能,是匿名函數(shù),對應 OC 的 block芍耘。

let a1 = [1,3,2].sorted(by: { (l: Int, r: Int) -> Bool in
    return l < r
})
// 如果閉包是唯一的參數(shù)并在表達式最后可以使用結尾閉包語法址遇,寫法簡化為
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
}
// 通過位置來使用閉包的參數(shù),最后簡化如下:
let a4 = [1,3,2].sorted { $0 < $1 }
print(a)

函數(shù)也是閉包的一種斋竞,函數(shù)的參數(shù)也可以是閉包倔约。@escaping 表示逃逸閉包,逃逸閉包是可以在函數(shù)返回之后繼續(xù)調用的坝初。@autoclosure 表示自動閉包浸剩,可以用來省略花括號。

函數(shù) func

函數(shù)可以作為另一個函數(shù)的參數(shù)鳄袍,也可以作為另一個函數(shù)的返回绢要。函數(shù)是特殊的閉包,在類拗小、結構體和枚舉中是方法重罪。

// 為參數(shù)設置默認值
func f1(p: String = "p") -> String {
    "p is \(p)"
}
// 函數(shù)作為參數(shù)
func f2(fn: (String) -> String, p: String) -> String {
    return fn(p)
}
print(f2(fn:f1, p: "d")) // p is d
// 函數(shù)作為返回值
func f3(p: String) -> (String) -> String {
    return f1
}
print(f3(p: "yes")("no")) // p is no

函數(shù)可以返回多個值,函數(shù)是可以嵌套的哀九,也就是函數(shù)里內可以定義函數(shù)剿配,函數(shù)內定義的函數(shù)可以訪問自己作用域外函數(shù)內的變量。inout 表示的是輸入輸出參數(shù)阅束,函數(shù)可以在函數(shù)內改變輸入輸出參數(shù)呼胚。defer 標識的代碼塊會在函數(shù)返回之前執(zhí)行。

訪問控制

在 Xcode 里的 target 就是模塊息裸,使用 import 可導入模塊蝇更。模塊內包含源文件沪编,每個源文件里可以有多個類、結構體簿寂、枚舉和函數(shù)等多種類型漾抬。訪問級別可以通過一些關鍵字描述,分為如下幾種:

  • open:在模塊外可以調用和繼承常遂。
  • public:在模塊外可調用不可繼承纳令,open 只適用類和類成員。
  • internal:默認級別克胳,模塊內可跨源文件調用平绩,模塊外不可調用。
  • fileprivate:只能在源文件內訪問漠另。
  • private:只能在所在的作用域內訪問捏雌。
    重寫繼承類的成員,可以設置成員比父類的這個成員更高的訪問級別笆搓。Setter 的級別可以低于對應的 Getter 的級別性湿,比如設置 Setter 訪問級別為 private,可以在屬性使用 private(set) 來修飾满败。

類型

數(shù)字 Int, Float

數(shù)字的類型有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
// 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^2
print(2_000_000) // 2000000

布爾數(shù) Bool

布爾數(shù)有 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 = """
Hi!
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))

Unicode、Character 和 SubString 等內容參見官方字符串文檔: Strings and Characters — The Swift Programming Language (Swift 5.1)

枚舉

Swift的枚舉有類的一些特性,比如計算屬性、實例方法腕窥、擴展、遵循協(xié)議等等朗鸠。

enum E1:String, CaseIterable {
    case e1, e2
}
// 關聯(lián)值
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 協(xié)議可迭代
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)
    }
}

泛型

泛型可以減少重復代碼秒拔,是一種抽象的表達方式。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 s = S1(arr: ["zero"])
s.add("one")
s.add("two")
print(s.arr) // ["zero", "one", "two"]

關聯(lián)類型

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

不透明類型

不透明類型會隱藏類型,讓使用者更關注功能焙压。不透明類型和協(xié)議很類似鸿脓,不同的是不透明比協(xié)議限定的要多抑钟,協(xié)議能夠對應更多類型。

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

類型轉換

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

類和結構體

類可以定義屬性、方法拨黔、構造器蛔溃、下標操作篱蝇。類使用擴展來擴展功能贺待,遵循協(xié)議。類還以繼承零截,運行時檢查實例類型麸塞。

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)

結構體

結構體是值類型,可以定義屬性涧衙、方法哪工、構造器、下標操作弧哎。結構體使用擴展來擴展功能雁比,遵循協(xié)議。

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 是屬性觀察器非洲,可以在屬性值設置前后插入自己的邏輯處理鸭限。

方法

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 不能赡麦。在協(xié)議中需用 static 來修飾。

繼承

類能繼承另一個類帕识,繼承它的方法泛粹、屬性等。

// 類繼承
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 關鍵字指代父類。

函數(shù)式

map

map 可以依次處理數(shù)組中元素伪货,并返回一個處理后的新數(shù)組们衙。

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

使用 compactMap 可以過濾 nil 的元素钾怔。flatMap 會將多個數(shù)組合成一個數(shù)組返回。

filter

根據(jù)指定條件返回

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) // 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 {
        $0.i
    }
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矾利,遍歷是無序的
et 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)
}
/*
 10
 8
 6
 4
 2
 0
 */

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 跳過當前作用域蜓谋,繼續(xù)下個循環(huán)

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 // 繼續(xù)到下個 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
 one
 */
// 枚舉
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 : // 枚舉關聯(lián)值梦皮,使用 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

集合

數(shù)組 [1, 2, 3]

數(shù)組是有序集合

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實現(xiàn)在  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"]

Sets Set<Int>

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 d = [
    "k1": "v1",
    "k2": "v2"
]
d["k3"] = "v3"
d["k4"] = nil
print(d) // ["k2": "v2", "k3": "v3", "k1": "v1"]
for (k, v) in d {
    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 d.isEmpty == false {
    print(d.count) // 3
}

操作符

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

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 協(xié)議可以使用 == 和 != 來判斷是否相等

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

類需要實現(xiàn) == 函數(shù)

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)

三元 _ ? _ : _

簡化 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 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

運算符函數(shù)包括前綴運算符笔横、后綴運算符竞滓、復合賦值運算符以及等價運算符。另吹缔,還可以自定義運算符商佑,新的運算符要用 operator 關鍵字進行定義,同時要指定 prefix厢塘、infix 或者 postfix 修飾符茶没。

特性

模式

單例模式

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

系統(tǒng)

版本兼容

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

Codable

JSON 沒有 id 字段

如果SwiftUI要求數(shù)據(jù)Model都是遵循Identifiable協(xié)議的,而有的json沒有id這個字段晚碾,可以使用擴展struct的方式解決:

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

編程范式

Combine響應式編程范式

介紹

WWDC 2019蘋果推出Combine抓半,Combine是一種響應式編程范式,采用聲明式的Swift API格嘁。官方文檔鏈接 Combine | Apple Developer Documentation 琅关。還有 Using Combine 這里有大量使用示例,內容較全讥蔽。官方討論Combine的論壇 Topics tagged combine 涣易。StackOverflow上相關問題 Newest 'combine' Questions

WWDC上關于Combine的Session如下:

和Combine相關的Session:

也就是你寫代碼不同于以往命令式的描述如何處理數(shù)據(jù)冶伞,而是要去描述好數(shù)據(jù)會經過哪些邏輯運算處理新症。這樣代碼更好維護,可以有效的減少嵌套閉包以及分散的回調等使得代碼維護麻煩的苦惱响禽。

聲明式和過程時區(qū)別可見如下代碼:

// 所有數(shù)相加
// 命令式思維
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 的三個核心概念

  • 發(fā)布者
  • 訂閱者
  • 操作符

簡單舉個發(fā)布數(shù)據(jù)和類屬性綁定的例子:

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)

使用場景

網絡請求

網絡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加到下面數(shù)組內。
    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
        true
      }
      .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)
  }
}
Timer

使用方式如下:

let timePb = Timer.publish(every: 1.0, on: RunLoop.main, in: .default)
let timeSk = timePb.sink { r in
    print("r is \(r)")
}
let cPb = timePb.connect()
SwiftUI

使用方式如下:

struct aView: View {
  @State private var currentVl = "vl"
  var body: some View {
    Text("string is \(currentVl)")
      .onReceive(currentPublisher) { newVl in
        self.currentVl = newVl
      }
  }
}

庫的選擇與使用說明

數(shù)據(jù)庫

GitHub - stephencelis/SQLite.swift: A type-safe, Swift-language layer over SQLite3.
GitHub - groue/GRDB.swift: A toolkit for SQLite databases, with a focus on application development

代碼規(guī)范

參考:

多用靜態(tài)特性贮竟。swift 在編譯期間所做的優(yōu)化比 OC 要多丽焊,這是由于他的靜態(tài)派發(fā)、泛型特化咕别、寫時復制這些靜態(tài)特性決定的技健。另外通過 final 和 private 這樣的表示可將動態(tài)特性轉化為靜態(tài)方式,編譯開啟 WMO 可以自動推導出哪些動態(tài)派發(fā)可轉化為靜態(tài)派發(fā)惰拱。

如何避免崩潰雌贱?

  • 字典:用結構體替代
  • Any:可用泛型或關聯(lián)關聯(lián)類型替代
  • as? :少用 AnyObject,多用泛型或不透明類型
  • !:要少用

好的實踐偿短?

  • 少用繼承欣孤,多用 protocol
  • 多用 extension 對自己代碼進行管理

最佳實踐

開源例子

macoOS

官方提供的兩個例子翔冀, Creating a macOS App 导街, Building a Great Mac App with SwiftUI (有table和
LazyVGrid的用法)。

三欄結構架子搭建纤子,代碼如下:

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

顯示效果如下:
[圖片上傳失敗...(image-356ab2-1639739251921)]

打開瀏覽器顯示指定網頁的代碼

NSWorkspace.shared.open(URL(string: "https://github.com/ming1016")!)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末搬瑰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子控硼,更是在濱河造成了極大的恐慌泽论,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卡乾,死亡現(xiàn)場離奇詭異翼悴,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門鹦赎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谍椅,“玉大人,你說我怎么就攤上這事古话〕裕” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵陪踩,是天一觀的道長杖们。 經常有香客問我,道長肩狂,這世上最難降的妖魔是什么摘完? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮傻谁,結果婚禮上孝治,老公的妹妹穿的比我還像新娘。我一直安慰自己栅螟,他們只是感情好荆秦,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著力图,像睡著了一般步绸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吃媒,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天瓤介,我揣著相機與錄音,去河邊找鬼赘那。 笑死刑桑,一個胖子當著我的面吹牛,可吹牛的內容都是我干的募舟。 我是一名探鬼主播祠斧,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拱礁!你這毒婦竟也來了琢锋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呢灶,失蹤者是張志新(化名)和其女友劉穎吴超,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸯乃,經...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡鲸阻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸟悴。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陈辱,死狀恐怖,靈堂內的尸體忽然破棺而出遣臼,到底是詐尸還是另有隱情性置,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布揍堰,位于F島的核電站,受9級特大地震影響嗅义,放射性物質發(fā)生泄漏屏歹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一之碗、第九天 我趴在偏房一處隱蔽的房頂上張望蝙眶。 院中可真熱鬧,春花似錦褪那、人聲如沸幽纷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽友浸。三九已至,卻和暖如春偏窝,著一層夾襖步出監(jiān)牢的瞬間收恢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工祭往, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伦意,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓硼补,卻偏偏與公主長得像驮肉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子已骇,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內容