尽量防止将办法的引证传递给一个承受@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…
过错代码
以下代码的写法导致getUserName
和getUserPicture
同步履行。
只需getUserName
履行结束才会履行getUserPicture
import Foundation
Task {
let userName = await getUserName()
let userPicture = await getUserPicture()
updateUI(userName: userName, userPicture: userPicture)
}
正确代码
采用结构化并发语法 async let,在需求运用结果的当地再await
这样能确保 getUserName
和 getUserPicture
并发履行,一起去拿结果
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…
正确代码
-
@MainActor
能够运用在类型、办法、闭包中等当地。 - 运用了
@MainActor
之后,编译器会确保所需求的操作是运行在主线程 @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。要注意循环引证
过错代码
- Task主动捕获了self的引证
- await notification这个for循环一直在监听AsyncSequence notifications,而AsyncSequence并不会中止
- 只需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)
}
}
}
}
正确代码
- 运用捕获列表,在Task里捕获weak self
- 由于形成循环引证的底子在于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(大多数情况下)
过错代码
-
Task.detached
会忽略一切上下文 - 运用
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…
过错代码
- 不运用lazy,整个代码需求履行完10000次
- 咱们其实只需求履行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-…
要了解?和!的差异
- ?被叫做:可选链,有值时便是那个值,不然是nil
- !被叫做:强制解包,有值时便是那个值,不然溃散
// #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
- 运用可选绑定能够防止后期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-…
闭包捕获值时是捕获变量,不管该值时值类型仍是引证类型
- 闭包默认会捕获值的变量,不管是值类型仍是引证类型,只需外部经过变量修正了值,闭包内也会相应修正
var someInteger = 2
let closure = { in
print(someInteger)
}
someInteger = 3
closure() // prints "3"
- 能够经过捕获列表捕获值,此时捕获的值是闭包创立时外部变量的值
// #01 - Capturing a Variable
var someInteger = 2
let closure = { [someInteger] in
print(someInteger)
}
someInteger = 3
closure() // prints "2"
注意闭包引起的循环引证及内存走漏问题
- ViewController持有Timer,Timer持有闭包,闭包持有self,形成了循环引证
- 经过捕获列表捕获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的闭包
- 闭包分为逃逸闭包(escaping)和非逃逸闭包(nonescaping)
- 逃逸闭包有或许形成循环引证,非逃逸闭包会在办法体内被消耗,不会形成循环引证
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…
- 这儿咱们笼统了Servicing协议,并分别完成了Service和MockedService用于不同的意图
- 运用协议编程能够解耦和进步扩展性
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结构
- 创立处理逻辑的ViewModel
- 将Service和Formatter逻辑转移到ViewModel中
- 增加一个数据变化时更新UI的回调
- 把获取数据和格式化数据的逻辑也转移到ViewModel中
- 把UIViewController当成View,给它创立一个ViewModel的特点
- 调用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)
}
}
}