尽量防止将办法的引证传递给一个承受@escaping闭包的函数

www.swiftwithvincent.com/blog/bad-pr…

过错代码

publisher.sink(receiveValue: handle(value:)).store(in: &cancellables)

等价于

publisher.sink(receiveValue: {self.handle(value:$0)}).store(in: &cancellables)

编译器主动捕获了self,形成了循环引证self->publisher->closuer->self

正确代码

class ViewModel {
    var cancellables = Set<AnyCancellable>()
    init() {
        publisher
        .sink(receiveValue: {[weak self] in
            guard let self else { return }
            self.handle(value:$0)}
        )
        .store(in: &cancellables)
    }
    func handle(value: String) {
        // `self` can be used here
    }
}

不要一有时机就让异步代码履行

www.swiftwithvincent.com/blog/discov…

过错代码

以下代码的写法导致getUserNamegetUserPicture同步履行。

只需getUserName履行结束才会履行getUserPicture

import Foundation
Task {
    let userName = await getUserName()
    let userPicture = await getUserPicture()
    updateUI(userName: userName, userPicture: userPicture)
}

正确代码

采用结构化并发语法 async let,在需求运用结果的当地再await

这样能确保 getUserNamegetUserPicture 并发履行,一起去拿结果

import Foundation
Task {
    async let userName = getUserName()
    async let userPicture = getUserPicture()
    await updateUI(userName: userName, userPicture: userPicture)
}

字符串判空多运用isEmpty而不是count > 0

www.swiftwithvincent.com/blog/bad-pr…

过错代码

运用count办法判空,在底层会遍历整个字符串获取其长度

当遇到超长字符串或许有许多字符的字符串时它的耗时会很长,影响功能

if myString.count > 0 {
    // `myString` isn't empty
}

正确代码

运用isEmpty只需求判别是否至少包含一个字符,不受字符串长度的影响

if myString.isEmpty == false {
    // `myString` isn't empty
}

由单个元素构成的Array,多运用CollectionOfOne而不是手动构建

www.swiftwithvincent.com/blog/discov…

过错代码

import Foundation
let someNumbers = [1, 2, 3]
let moreNumbers = someNumbers + [4]

正确代码

在运用for循环的操作中CollectionOfOne有更好的功能

import Foundation
let someNumbers = [1, 2, 3]
let moreNumbers = someNumbers + CollectionOfOne(4)

长于运用#error操控一些有必要修正的代码

www.swiftwithvincent.com/blog/discov…

正确代码

#error能够让编译器在编译期就露出问题代码

import Foundation
#error("You can get your apiKey at https://dev.myapi.com/")
let apiKey = "create_your_own_api_key"

关于大数的表明,长于运用分割符

www.swiftwithvincent.com/blog/discov…

let bigNumber = 123_456_789

写好Swift代码的三条提示

www.swiftwithvincent.com/blog/3-tips…

考虑运用多行字符串语法输出字符串

// #01 – Multiline String
let multilineString = """
1st line
2nd line
3rd line
"""

单个泛型参数的函数考虑运用some

// #02 – Opaque Arguments
func handle(value: some Identifiable) {
    /* ... */
}

关于会抛出过错的单元测试,考虑将case函数修正为throws,在case函数内经过try调用需求掩盖的函数

// #03 – Throwing Tests
class Test: XCTestCase {
    func test() throws {
        XCTAssertEqual(try throwingFunction(), "Expected Result")
    }
}

考虑运用带有相关类型的enum重构互斥的逻辑

www.swiftwithvincent.com/blog/how-to…

过错代码

// 过错1 各种特点杂糅在一起
struct BlogPost {
    // Common properties
    var title: String 
    // Article properties
    var wordCount: Int?
    // Video properties
    var videoURL: URL?
    var duration: String?
}
// 过错2 分离了Video和Article 但并未给出互斥逻辑
struct ArticleMetadata {
    var workdCount: Int
}
struct VideoMetadata {
    var videoURL: URL
    var duration: String
}
struct BlogPost {
    var title: String
    var articleMetadata: ArticleMetadata?
    var videoMetadata: VideoMetadata?
}

正确代码

// 带有相关类型的枚举
enum Metadata {
    case article(wordCount: Int)
    case video(videoURL: URL, duration: String)
}
// 视频和文章是互斥的
struct BlogPost {
    var title: String
    var metadata: Metadata
}
let correctBlogPost = BlogPost(
    title: "My Awesome Video",
    metadata: .video(
        videoURL: URL(string: "https://mywebsite.com/myVideo.mp4")!,
        duration: "4:35"
    )
)

考虑运用dump打印引证类型,运用print打印值类型

www.swiftwithvincent.com/blog/discov…

过错代码

class Person {
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    var name: String
    var age: Int
}
let me = Person(name: "Vincent", age: 32)
print(me)

正确代码

class Person {
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    var name: String
    var age: Int
}
let me = Person(name: "Vincent", age: 32)
dump(me)

关于需求在主线程履行的结构,考虑运用 @MainActor

www.swiftwithvincent.com/blog/discov…

正确代码

  1. @MainActor能够运用在类型、办法、闭包中等当地。
  2. 运用了@MainActor之后,编译器会确保所需求的操作是运行在主线程
  3. @MainActor只会在运用了Swift并发的异步代码中有用,关于咱们自己写的completionHandler或许运用了Combine的代码,它是无效的。需求咱们手动切换
import Foundation
@MainActor
class ViewModel: ObservableObject {
    @Published var text = ""
    func updateText() {
        Task {
            let newText = await fetchFromNetwork()
            // guaranteed to run on the Main Thread
            text = newText
        }
    }
}
@MainActor
class ViewModel: ObservableObject {
    @Published var text = ""
    func updateText() {
        fetchFromNetwork { [weak self] newText in
            // ⚠️ @MainActor has no effect here
            // 需求咱们切换到主线程履行以下代码
            self?.text = newText
        }
    }
}

运用async/await时的3个提示

www.swiftwithvincent.com/blog/three-…

不要一有或许就运行异步代码

过错代码

下一个await只需等上一个await完结之后才开端履行

import Foundation
// the next call starts only after the previous one has finished.
let user = await getUser()
let address = await getAddress(of: user)
// 在 getAddress 履行结束之后,getPaymentMethod才会履行
let paymentMethod = await getPaymentMethod(of: user)
print("\(address) \(paymentMethod)”)

正确代码

运用结构化语法async let,能确保多个异步操作一起履行

import Foundation
// #01 – Not running code concurrently when possible
// This call will run first…
let user = await getUser()
// ...and after it has completed, the
// two others will then run concurrently
async let address = getAddress(of: user)
async let paymentMethod = getPaymentMethod(of: user)
// 只在运用结果的当地用await关键字
await print("\(address) \(paymentMethod)”)

要时间记住:Task会主动捕获self。要注意循环引证

过错代码

  1. Task主动捕获了self的引证
  2. await notification这个for循环一直在监听AsyncSequence notifications,而AsyncSequence并不会中止
  3. 只需AsyncSequence不中止,Task就不会退出,Task不退出它所捕获的self就不会被释放,从而形成内存走漏
@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }
    func listenToNotifications() {
        Task {
            let notifications = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            for await notification in notifications {
                // 这儿运用handle必定需求有self
                // 这儿实际上捕获了self
                handle(notification)
            }
        }
    }
}

正确代码

  1. 运用捕获列表,在Task里捕获weak self
  2. 由于形成循环引证的底子在于AsyncSequence不中止,所以咱们需求在for in循环中解包self,打破async for in的无限循环。
// #02 – Not understanding that `Task` automatically captures `self`
@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }
    func listenToNotifications() {
        Task { [weak self] in
            // 假如在这儿解包self,仍然无法使Task中止
            // guard let self else { return }
            // Here the `Task` still holds a local
            // strong reference to `self` forever 
            let notifications = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            for await notification in notifications {
                guard let self else { return }
                self.handle(notification)
            }
        }
    }
}

在需求捕获上下文的当地主张运用Task,不然运用Task.detached(大多数情况下)

过错代码

  1. Task.detached 会忽略一切上下文
  2. 运用Task.detached后,listenToNotifications将不会运行于异步上下文。需求在调用async办法的当地运用await关键字
@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }
    func listenToNotifications() {
        Task.detached { [weak self] in
            let notifications = await NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            for await notification in notifications {
                guard let self else { return }
                await self.handle(notification)
            }
        }
    }
}

正确代码

Task继承了MainActor的上下文,所以Task内不需求再await self.handle

// #03 – Using `Task.detached` when not needed
@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }
    func listenToNotifications() {
        Task { [weak self] in
            let notifications = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            for await notification in notifications {
                guard let self else { return }
                self.handle(notification)
            }
        }
    }
}

多考虑运用LazySequence,尤其是在很多的CPU操作时

www.swiftwithvincent.com/blog/discov…

过错代码

  1. 不运用lazy,整个代码需求履行完10000次
  2. 咱们其实只需求履行15次就能够,其他的9985次完全是无意义的。并且还消耗CPU
import Foundation
(1...10_000)
    .map { $0 * $0 } // executed 10000 times
    .filter { $0.isMultiple(of: 5) } // executed 10000 times
    .first (where: { $0 > 100 })

正确代码

这样只需求履行最初的15次就能够

import Foundation
(1...10_000)
    .lazy
    .map { $0 * $0 } // executed 15 times
    .filter { $0.isMultiple(of: 5) } // executed 15 times
    .first (where: { $0 > 100 })

运用Optionals时的3个体型

www.swiftwithvincent.com/blog/three-…

要了解?和!的差异

  1. ?被叫做:可选链,有值时便是那个值,不然是nil
  2. !被叫做:强制解包,有值时便是那个值,不然溃散
// #01 - Not understanding the difference between `?` and `!`
let optionalString: String? = Bool.random() ? "Hello, world!" : nil
// Optional Chaining
optionalString?.reversed() // will return `nil` if `optionalString` is `nil`
// Force Unwrapping
optionalString!.reversed() // will crash if `optionalString` is `nil`

关于可选类型,多运用可选绑定而不是判别是不是nil

  1. 运用可选绑定能够防止后期if条件改动时的问题
// #02 – Not using Optional Binding
if let optionalString {
    // `optionalString` is now of
    // type `String` inside this scope
    print(optionalString.reversed())
}

不要任何当地都运用可选。确认有值的当地就不要运用可选值

import Foundation
// #03 - Using an Optional when it is not needed
struct Person {
    let id: UUID
    let name: String
    // 初始化办法的name已经是个确认值,name就不必声明成String?
    init(name: String) {
        self.id = UUID()
        self.name = name
    }
}

对可选值进行单元测试时尽量运用XCTUnwrap,而不是自己解包判别

www.swiftwithvincent.com/blog/discov…

import XCTest
class MyTests: XCTestCase {
    func test() throws {
        let myData = [1, 2, 3]
        let first = try XCTUnwrap(myData.first)
        XCTAssert(first<3)
    }
}

运用Closure时的3个提示

www.swiftwithvincent.com/blog/three-…

闭包捕获值时是捕获变量,不管该值时值类型仍是引证类型

  1. 闭包默认会捕获值的变量,不管是值类型仍是引证类型,只需外部经过变量修正了值,闭包内也会相应修正
var someInteger = 2
let closure = { in
    print(someInteger)
}
someInteger = 3
closure() // prints "3"
  1. 能够经过捕获列表捕获值,此时捕获的值是闭包创立时外部变量的值
// #01 - Capturing a Variable
var someInteger = 2
let closure = { [someInteger] in
    print(someInteger)
}
someInteger = 3
closure() // prints "2"

注意闭包引起的循环引证及内存走漏问题

  1. ViewController持有Timer,Timer持有闭包,闭包持有self,形成了循环引证
  2. 经过捕获列表捕获weak self来打破循环引证
// #02 – Retain Cycles
import UIKit
class ViewController: UIViewController {
    var timer: Timer?
    let label = UILabel()
    let formatter = DateFormatter()
    override func viewDidLoad() {
        super.viewDidLoad()
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            guard let self else { return }
            let now = Date()
            self.label.text = self.formatter.string(from: now)
        }
    }
}

注意区别 escaping 和 nonescaping的闭包

  1. 闭包分为逃逸闭包(escaping)和非逃逸闭包(nonescaping)
  2. 逃逸闭包有或许形成循环引证,非逃逸闭包会在办法体内被消耗,不会形成循环引证
import Foundation
// 03 - Escaping and non-escaping closures
extension ViewController {
    func decorateTimeWithEmojis() -> [String] {
        ["⏲️", "⏰", "⏳"].map { emoji in            
            let now = Date()
            return "\(emoji) \(self.formatter.string(from: now))"
        }
    }
}

关于枚举类型,最好完成CaseInterable

www.swiftwithvincent.com/blog/discov…

关于完成了CaseIterable协议的非相关枚举,编译器能够主动生成allCases静态特点

import Foundation
enum Direction: CaseIterable {
    case north
    case south
    case east
    case west
}
Direction.allCases // [.north, .south, .east, .west]

多运用Swift中的泛型,对逻辑进行笼统

www.swiftwithvincent.com/blog/discov…

运用泛型能够对逻辑进行笼统,而不必关怀详细的类型

struct Stack<Element> {
    private var values = [Element]()
    mutating func push(_ value: Element) {
        values.append(value)
    }
    mutating func pop() -> Element? {
        return values.removeLast()
    }
}
let stackOfInt = Stack<Int>()
let stackOfString = Stack<String>()
let stackOfPerson = Stack<Person>()

长于运用PropertyWrapper来封装简略的固有逻辑

www.swiftwithvincent.com/blog/discov…

@propertyWrapper
struct Trimmed {
    var string: String
    init(wrappedValue: String) {
        self.string = wrappedValue
    }
    var wrappedValue: String {
        get {
            string.trimmingCharacters(
                in: .whitespacesAndNewlines
            )
        }
        set {
            string = newValue
        }
    }
}
import Foundation
struct API {
    @Trimmed var url: String
}
var api = API(url: "https://myapi.com/ ")
URL(string: api.url) // valid URL ✅

关于KeyPath你不知道的

www.swiftwithvincent.com/blog/5-thin…

KeyPath能够作为闭包或函数传递给高阶函数的参数

编译器会主动将KeyPath转换成闭包

struct Person {
    let name: String
    var age: Int
}
let people = [
    Person(name: "John", age: 30),
    Person(name: "Sean", age: 14),
    Person(name: "William", age: 50),
]
people.map { $0.name }
people.map(\.name)

有多种类型的KeyPath

// if struct Person
let readOnlyKeyPath = \Person.name // KeyPath<Person, String>
let readWriteKeyPath = \Person.age // WritableKeyPath<Person, Int>
// if class Person
let readWriteKeyPath = \Person.age // ReferenceWritableKeyPath<Person, Int>

KeyPath能够作为下标操作符

struct Person {
    let name: String
    var age: Int
}
let people = [
    Person(name: "John", age: 30),
    Person(name: "Sean", age: 14),
    Person(name: "William", age: 50),
]
let subscriptKeyPath = \[Person].[1].name
people[keyPath: subscriptKeyPath] // "Sean"

运用KeyPath写简略的DSL

func > <Root, Value: Comparable>(
    _ leftHandSide: KeyPath<Root, Value>,
    _ rightHandSide: Value
    ) -> (Root) -> Bool {
    return { $0[keyPath: leftHandSide] > rightHandSide }
}
people.filter(\.age > 18)

将KeyPath作为动态成员查找的参数

来看一下怎么直接访问Order中的address的特点,就好像它是Order的特点一样

import Foundation
struct Address {
    let city: String
    let country: String
}
@dynamicMemberLookup
struct Order {
    let customer: Person
    let address: Address
    /* ... */
    subscript<T>(dynamicMember keyPath: KeyPath<Address, T>) -> T {
        address[keyPath: keyPath]
    }
}
let order = Order(
    customer: Person(name: "Vincent", age: 32),
    address: Address(city: "Lyon", country: "France")
)
order.city // equalivalent to `order.address.city`
order.country // equalivalent to `order.address.country`

关于异步操作,运用数据结构时考虑运用Actor

www.swiftwithvincent.com/blog/discov…

actor内部会协助咱们处理数据竞赛,从而削减代码量

actor ImageCache {
    private var cache = [UUID: UIImage]()
    func save(image: UIImage, withID id: UUID) {
        cache[id] = image
    }
    func getImage(for id: UUID) -> UIImage? {
        cache[id]
    }
}
let imageCache = ImageCache()
Task.detached {
    await imageCache.save(image: firstImage, withID: firstImageID)
}
Task.detached {
    await imageCache.save(image: secondImage, withID: secondImageID)
}
let cachedImage = await imageCache.getImage(for: firstImageID)

异步代码,有条件便是用async、await吧

www.swiftwithvincent.com/blog/discov…

async、await是Swift异步编程的根底。

它基于协程,比现有的基于线程的handler有更好的功能,一起运用也更简略

// Synchronous functions
func add(_ first: Int, _ second: Int) -> Int {
    return first + second
}
func longToExecute() -> Int {
    var result = 0
    for i in 0...1_000_000 {
        result += i
    }
    return result
}
// Asynchronous function
func loadFromNetwork() async -> Data {
    let url = URL(string: "https://myapi.com/endpoint")!
    let (data, _) = try! await URLSession.shared.data(from: url)
    return data
}
// Calling `async` functions
func anAsyncFunction() async {
    await anotherAsyncFunction()
}
func aSynchronousFunction() {
    Task {
        await anAsyncFunction()
    }
}

多运用协议编程的思想,进步笼统能力

www.swiftwithvincent.com/blog/discov…

  1. 这儿咱们笼统了Servicing协议,并分别完成了Service和MockedService用于不同的意图
  2. 运用协议编程能够解耦和进步扩展性
class Service: Servicing {
    func getData(
        _ completion: @escaping (Result<String, Error>) -> Void
    ) {
        /* some networking code */
    }
}
class ViewModel: ObservableObject {
    @Published var data: String? = nil
    @Published var error: Error? = nil
    private let service: Servicing
    init(service: Servicing) {
        self.service = service
    }
    func fetchData() {
        service.getData { [weak self] result in
            switch result {
            case .success(let data):
                self?.data = data
            case .failure(let error):
                self?.error = error
            }
        }
    }
}
class MockedService: Servicing {
    var getDataCallCounter = 0
    var result: Result<String, Error>!
    func getData(
        _ completion: @escaping (Result<String, Error>) -> Void
    ) {
        getDataCallCounter += 1
        completion(result)
    }
}
final class ViewModelTests: XCTestCase {
    func testSuccessCase() {
        // Given
        let mockedService = MockedService()
        mockedService.result = .success("Hello, world!")
        let viewModel = ViewModel(service: mockedService)
        // When
        viewModel.fetchData()
        // Then
        XCTAssertEqual(mockedService.getDataCallCounter, 1)
        XCTAssertEqual(viewModel.data, "Hello, world!")
        XCTAssertNil(viewModel.error)
    }
}

最简略的MVVM形式

www.swiftwithvincent.com/blog/discov…

MVVM形式便是将逻辑处理封装到VM层,Model和View只做简略的业务解析和展现即可

过错代码

看以下代码,ViewController的职责比较多
获取数据、格式化数据、展现数据

class ViewController: UIViewController {
    let service = Service()
    let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        return formatter
    }()
    @IBOutlet weak var label: UILabel!
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        service.fetchNumber{ [weak self] number in 
            let formatted = self?.formatter.string(for: number)
            self?.label.text= formatted
        }
    }
}

正确代码

6步创立一个MVVM结构

  1. 创立处理逻辑的ViewModel
  2. 将Service和Formatter逻辑转移到ViewModel中
  3. 增加一个数据变化时更新UI的回调
  4. 把获取数据和格式化数据的逻辑也转移到ViewModel中
  5. 把UIViewController当成View,给它创立一个ViewModel的特点
  6. 调用viewModel的一些API以及设置ViewModel的一些回调处理
class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    // 5.
    let viewModel = ViewModel()
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.updateUI = { [weak self] newData in
            self?.label.text = newData
        }
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        viewModel.fetchData()
    }
}
// 1. 
class ViewModel {
    // 2.
    let service = Service()
    let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        return formatter
    }()
    // 3.
    var updateUI: ((_ newDataToDisplay: String?) -> Void)?
    // 4.
    func fetchData() {
        service.fetchNumber { [weak self] newNumber in
            let formatted = self?.formatter.string(for: newNumber)
            self?.updateUI?(formatted)
        }
    }
}