使用Swift开发,你应该知道的好习惯(二)

340 阅读5分钟

好习惯一

在一个类型里面存储两个不同类型的值

www.swiftwithvincent.com/blog/storin…

自定义一个泛型enum Either存储任意两个不同类型的值

import SwiftUI

struct Movie: Identifiable {
    let id = UUID()
    // ....
}

struct Advertisement: Identifiable {
    let id = UUID()
    // ...
}

struct ContentView: View {
    
    @State var content: [Movie]
    
    var body: some View {
        List(content) { movie in
            // display the `movie`
        }
    }
}

enum Either<Left, Right> {
    case left(Left)
    case right(Right)
}

extension Either: Identifiable where Left: Identifiable, 
                                     Right: Identifiable,
                                     Left.ID == Right.ID {
    var id: Left.ID {
        switch self {
        case .left(let left):
            return left.id
        case .right(let right):
            return right.id
        }
    }
}

方便的构建一个反向滚动的列表

www.swiftwithvincent.com/blog/buildi…

通过编写一个上下翻转的ViewModifier,先对列表进行翻转,然后对列表中的每一项进行翻转

这种场景非常适合用来做消息聊天列表

import SwiftUI

struct ChatMessage: View {
    
    let text: String
    
    var body: some View {
        HStack {
            Text(text)
                .foregroundStyle(.white)
                .padding()
                .background(.blue)
                .clipShape(
                    RoundedRectangle(cornerRadius: 16)
                )
                .overlay(alignment: .bottomLeading) {
                    Image(systemName: "arrowtriangle.down.fill")
                        .font(.title)
                        .rotationEffect(.degrees(45))
                        .offset(x: -10, y: 10)
                        .foregroundStyle(.blue)
                }
            Spacer()
        }
        .padding(.horizontal)
    }
}

struct BadChatView: View {
    
    @State var data = (0...20).map { $0 }
    
    var body: some View {
        ScrollViewReader { scrollView in
            List(data.reversed(), id: \.self) { int in
                ChatMessage(text: "\(int)")
                    .listRowSeparator(.hidden)
                    .id(int)
                    .onAppear {
                        if int == data.last {
                            loadMoreData()
                        }
                    }
            }
            .listStyle(.plain)
            .onAppear {
                scrollView.scrollTo(data.first!)
            }
        }
    }
    
    func loadMoreData() {
        guard data.count < 40 else { return }
        
        let additionalData = (21...40).map { $0 }
        
        data.append(contentsOf: additionalData)
    }
}

struct GoodChatView: View {
    
    @State var data = (0...20).map { $0 }
    
    var body: some View {
        List(data, id: \.self) { int in
            ChatMessage(text: "\(int)")
                .flippedUpsideDown()
                .listRowSeparator(.hidden)
                .onAppear {
                    if int == data.last {
                        loadMoreData()
                    }
                }
        }
        .listStyle(.plain)
        .flippedUpsideDown()
    }
    
    func loadMoreData() {
        guard data.count < 40 else { return }
        
        let additionalData = (21...40).map { $0 }
        
        data.append(contentsOf: additionalData)
    }
}

struct FlippedUpsideDown: ViewModifier {
    func body(content: Content) -> some View {
        content
            .rotationEffect(.radians(Double.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

extension View {
    func flippedUpsideDown() -> some View {
        modifier(FlippedUpsideDown())
    }
}

3个很有用的开发Swift的建议

www.swiftwithvincent.com/blog/here-a…

不要误以为enumerate的第一个参数是下标

我们遍历一个集合的时候经常会使用下面的方式。但是这样获取的i并不是集合的下标,它只是迭代器的下标,虽然它大多数情况下和集合下标相等

var ingredients = ["potatoes", "cheese", "cream"]

for (i, ingredient) in ingredients.enumerated() {
    print("ingredient number is \(i + 1) is \(ingredient)")
}

建议使用下面的两种方式准确获取index下标

// 使用zip缝合index和值value
// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]
// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()

for (i, ingredient) in zip(doubleIngredients.indices, doubleIngredients) {
    // Correctly use the actual indices of the subquence
    doubleIngredients[i] = "\(ingredient) X 2"
}
println(doubleIngredients)
// 使用 indexed方法同时遍历下标和值
// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]
// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()

for (i, ingredient) in doubleIngredients.indexed() {
    // Correctly use the actual indices of the subquence
    doubleIngredients[i] = "\(ingredient) X 2"
}
println(doubleIngredients)

使用标签跳出多层循环

let matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
let valueToFind = 5
searchValue: for row in matrix {
    for num in row {
        if num == valueToFind {
            print("Value \(valueToFind) found!")
            break searchValue
        }
    }
}

重写模式匹配运算符 ~= 支持自定义类型进行匹配

struct Circle {
    var radius: Double 
}
func ~= (pattern: Double, value:Circle) -> Bool {
    return value.radius == pattern
}
func ~= (pattern: ClosedRange<Double>, value: Circle) -> Bool {
    return pattern.containes(value.radius)
}
let myCircle = Circle(radius:5)
switch myCircle {
    case 5:
        print("Circle with a radius of 5")
    case 1...10:
        print("Circle with a radius between 1 and 10")
    default:
        print("Circle with a different radius")
}

可以使用Xcode15及以上版本预览iOS17的界面

www.swiftwithvincent.com/blog/previe…

import UIKit

@available(iOS 17, *) 
#Preview {
    let label = UILabel()
    label.text = "Hello, UIKit world!"
    return label
}

image.png

使用正则配合捕获组进行代码重构

www.swiftwithvincent.com/blog/how-to… 原始代码是这样的

image.png

先写一个String扩展

image.png

使用 NSLocalizedString\((".*"), comment: ""\) 查找,并使用$1.localized按照分组替换后,可循序重构成如下代码

image.png

开启Xcode的自动拼写检查

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

image.png

any代表遵循协议的任意类型,在编译时会生成Container类型。Any代表任意类型,在实际使用时需要as转换成具体类型

www.swiftwithvincent.com/blog/whats-…

// using `any`

protocol NetworkServicing {
    func fetchUserName() async -> String
}

class ViewModel {
    var userName: String?
    private var service: any NetworkServicing

    init(service: any NetworkServicing) {
        self.service = service
    }
    
    func fetchData() {
        Task {
            userName = await service.fetchUserName()
        }
    }
}

// using `Any`
let canContainAnything: [Any] = [
    "Hello",
    42,
    URL(string: "https://swift.org")!,
    { (arg: Int) in print(arg) },
]

if let string = canContainAnything.first as? String {
    print("It contains the string \(string)")
}

var userInfo: [AnyHashable : Any]?

使用Xcode更方便的测试通知

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

  1. 在工程中新建一个 .apns 的文件

image.png

image.png 2. 文件内容如下

需要将Simulator Target Bundle修改为你App的bundle id image.png 3. 通过将文件拖拽到模拟器 或者执行 xcrun simctl push booted test-notification.apns 来将通知推送到模拟器

当然你也可以使用类似的工具

  1. SmartPush github.com/shaojiankui…
  2. ApplePushTesting github.com/dourgulf/Ap…

善于使用Swift中的各种Formatters进行格式化展示

www.swiftwithvincent.com/blog/swift-…

// #01 - DateComponentsFormatter

import Foundation

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = true
formatter.includesTimeRemainingPhrase = true
formatter.allowedUnits = [.minute]
 
// Result: "About 5 minutes remaining"
let outputString = formatter.string(from: 300.0)

// #02 - DateIntervalFormatter

import Foundation

let formatter = DateIntervalFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none

// Create two dates that are exactly 1 day apart.
let startDate = Date()
let endDate = Date(timeInterval: 86400, since: startDate)

// Result: "03/03/2024 – 04/03/2024”
let outputString = formatter.string(from: startDate, to: endDate))

// #03 - MeasurementFormatter

import Foundation

let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "en_US")

let measurement = Measurement(
    value: 1.2,
    unit: UnitLength.kilometers
)

// Result: "0.746 mi"
let outputString = formatter.string(from: measurement)

// #04 - ByteCountFormatter

import Foundation

let formatter = ByteCountFormatter()
formatter.countStyle = .file

// Result: "1 GB"
let outputString = formatter.string(fromByteCount: 1_000_000_000)

// #05 - PersonNameComponentsFormatter

import Foundation

let formatter = PersonNameComponentsFormatter()

var components = PersonNameComponents()
components.givenName = "Vincent"
components.familyName = "Pradeilles"

// Result: "Vincent Pradeilles"
let firstOutputString = formatter.string(from: components)

formatter.locale = Locale(identifier: "ko_KR")

// Result: "Pradeilles Vincent"
let secondOutputString = formatter.string(from: components)

使用Swift宏生成EnvironmentKey和对应的默认值

www.swiftwithvincent.com/blog/using-…

依赖package:github.com/Wouter01/Sw…

我们定义一个EnvironmentKey的时候,需要定义一个自定义的struct,扩展EnvironmentValue写对应的getter和setter。有了上面的宏之后这些繁复的代码就可以在编译期自动完成

// Without the macro
import SwiftUI

struct UserNameEnvironmentKey: EnvironmentKey {
    static var defaultValue: String = "Anonymous"
}

extension EnvironmentValues {
    var userName: String {
        get {
            self[UserNameEnvironmentKey.self]
        }
        
        set {
            self[UserNameEnvironmentKey.self] = newValue
        }
    }
}

// With the macro
import SwiftUI
import SwiftUIMacros

@EnvironmentStorage
extension EnvironmentValues {
    var userName: String = "Anonymous"
}

做动画时,调用View的layoutIfNeeded让约束立即生效

www.swiftwithvincent.com/blog/do-you…

在我们使用AutoLayout时,如果在动画blok里面改变了约束,约束不会立即生效,这时就需要添加layoutIfNeeded立即计算约束

// First Example
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var blueView: UIView!
    @IBOutlet weak var blueViewHeightConstraint: NSLayoutConstraint!
        
    @IBAction func increaseHeightButtonTapped(_ sender: Any) {
        print(blueView.frame.height) // 240
        blueViewHeightConstraint.constant += 50
        print(blueView.frame.height) // 240
        view.layoutIfNeeded()
        print(blueView.frame.height) // 290
    }
}

// Second Example
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var blueView: UIView!
    @IBOutlet weak var blueViewHeightConstraint: NSLayoutConstraint!
        
    @IBAction func increaseHeightButtonTapped(_ sender: Any) {
        UIView.animate(withDuration: 0.3) {
            self.blueViewHeightConstraint.constant += 50
            self.view.layoutIfNeeded()
        }
    }
}

onTapGesture好用,可不要贪杯哦

www.swiftwithvincent.com/blog/be-car…

  1. onTapGesture可以给任意View添加点击事件,但是注意注意不要乱用
  2. 在明确需要用户操作的地方,还是使用Button吧,它不仅能提供视觉反馈,还有附带的障碍功能提示
// First Use Case

import SwiftUI

struct ContentView: View {
    
    @StateObject var viewModel = ViewModel()
  
    var body: some View {
        VStack {
            // ...
        }
        .onTapGesture {
           viewModel.prefetchData()
        }
    }
}

// Second Use Case (Bad)

import SwiftUI

struct ContentView: View {
       
    @State var showModal = false
    
    var body: some View {
        VStack {
            // ...
        }
        .sheet(isPresented: $showModal){
            // ...
        }
        .onTapGesture {
            showModal = true
        }
    }
}

// Second Use Case (Good)

import SwiftUI

struct ContentView: View {
       
    @State var showModal = false
    
    var body: some View {
        Button {
            showModal = true
        } label: {
            VStack {
                // ...
            }
        }
        .sheet(isPresented: $showModal){
            // ...
        }
    }
}

使用Xcode功能简单的模拟应用内购买

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

假设有如下的代码,我们需要显示三款产品并提供点击购买的功能 image.png 如果我们运行后,会发现什么都没有展示。这是因为我们没有配置文件

image.png 创建StoreKit Configuration File

在菜单,新建文件里选择如下 image.png 填写配置文件名称,后面会用到。如果已经上架过购买功能,最好把红框内的内容也勾选上 image.png 选择一种类型 image.png 填写名称和产品ID,产品ID是我们上面需要展示的ID image.png 填写产品的显示名称和价格

image.png

image.png 运行App的时候配置一下Scheme

image.png

image.png 运行起来之后就可以测试购买流程了

image.png

image.png

image.png

使用URLProtocol无侵入性的mock任意一个网络请求

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

// UserAPI.swift
import Foundation
// 定义一个User Model类
struct User: Decodable {
    let firstName: String
    let lastName: String
}
// 网络请求类
final class UserAPI {
    let endpoint = URL(string: "https://my-api.com")!
    
    let session: URLSession
    let decoder: JSONDecoder
    
    init(
        session: URLSession = URLSession.shared,
        decoder: JSONDecoder = JSONDecoder()
    ) {
        self.session = session
        self.decoder = decoder
    }
    
    func fetchUser() async throws -> User {
        return try await request(url: endpoint.appendingPathComponent("user/me"))
    }
    // 使用Session进行网络请求
    private func request<T>(url: URL) async throws -> T where T: Decodable {
        let (data, _) = try await session.data(from: url)
        return try decoder.decode(T.self, from: data)
    }
}

// MockURLProtocol.swift

import XCTest
// 定义一个用于Mock的URLProtocol
class MockURLProtocol: URLProtocol {
    // 重写一些方法
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            XCTFail("No request handler provided.")
            return
        }
        
        do {
            let (response, data) = try handler(request)
            
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            XCTFail("Error handling the request: \(error)")
        }
    }

    override func stopLoading() {}

}

// UserAPITests.swift

import Foundation
import XCTest

@testable import MyApp

class UserAPITests: XCTestCase {
    lazy var session: URLSession = {
        let configuration = URLSessionConfiguration.ephemeral
        configuration.protocolClasses = [MockURLProtocol.self]
        return URLSession(configuration: configuration)
    }()

    lazy var api: UserAPI = {
        UserAPI(session: session)
    }()

    override func tearDown() {
        MockURLProtocol.requestHandler = nil
        super.tearDown()
    }

    func testFetchUser() async throws{
        let mockData = """
        {
            "firstName": "Vincent",
            "lastName": "Pradeilles"
        }
        """.data(using: .utf8)!

        MockURLProtocol.requestHandler = { request in
            XCTAssertEqual(request.url?.absoluteString, "https://my-api.com/user/me")
            
            let response = HTTPURLResponse(
                url: request.url!,
                statusCode: 200,
                httpVersion: nil,
                headerFields: nil
            )!
            
            return (response, mockData)
        }

        let result = try await api.fetchUser()
        
        XCTAssertEqual(result.firstName, "Vincent")
        XCTAssertEqual(result.lastName, “Pradeilles")
    }
}

多使用 .isMultiple(of:) 进行条件判断,让意图更加明显

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

// Before
import Foundation

let myInteger = Int.random(in: 0...10)

if myInteger % 2 == 0 {
    print("it's even")
} else {
    print("it's odd")
}

// After
import Foundation

let myInteger = Int.random(in: 0...10)
// 代码看起来更加清晰,意图也变得更加明显
if myInteger.isMultiple(of: 2) {
    print("it's even")
} else {
    print("it's odd")
}

使用锁、钥匙 模式写更安全的代码

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

有瑕疵的代码

  1. 下面的代码 fetchNewsFeed 总是假定已经有token,但其实没有人保证这个
  2. 代码假设方法authenticate()将始终在方法fetchNewsFeed()之前调用
// 网络客户端
actor NetworkClient {
    var authToken: String?
    // 获取身份验证令牌
    func authenticate(
        login: String,
        password: String
    ) async {
        // ...
        self.authToken = authToken
    }
    // 使用身份验证令牌来检索一些实际数据
    func fetchNewsFeed() async throws -> NewsFeed {
        guard let authToken else {
            throw NetworkError.noToken
        }

        // ...
    }
}

完善的代码

希望有一种机制,只要属性authToken仍然设置为nil,就不可能调用fetchNewsFeed()方法

  1. 将NetworkClient拆成两部分:UnauthenticatedNetworkClient 和AuthenticatedNetworkClient。

  2. 在AuthenticatedNetworkClient中,我们将authToken设置为非可选类型

    • 这样为了初始化AuthenticatedNetworkClient,现在必须提供authToken
  3. 更新UnauthenticatedNetworkClient,使方法authenticate()返回一个AuthenticatedNetworkClient

actor UnauthenticatedNetworkClient {
    func authenticate(
        login: String,
        password: String
    ) async -> AuthenticatedNetworkClient {
        // ...
        return AuthenticatedNetworkClient(authToken: authToken)
    }
}

actor AuthenticatedNetworkClient {
    var authToken: String

    init(authToken: String) {
        self.authToken = authToken
    }

    func fetchNewsFeed() async throws -> NewsFeed {
        // ...
    }
}

使用 String的 compare方法比较字符串

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

  1. 每次调用.lowercased()方法时,它都会创建一个新的字符串副本,这可能会对性能产生负面影响
import Foundation

let searchQuery = "cafe"
let databaseValue = "Café"

let comparisonResult = searchQuery.compare(
    databaseValue,
    // 大小写不敏感、重音符号不敏感
    options: [.caseInsensitive, .diacriticInsensitive]
)

if comparisonResult == .orderedSame {
    // ...
}

理解 translatesAutoresizingMaskIntoConstraints

www.swiftwithvincent.com/blog/do-you…

Masonry或者SnapKit都是使用的AutoLayout技术,我们在使用Masonry或者SnapKit的时候经常需要将View的translatesAutoresizingMaskIntoConstraints设置为false

这里我们来了解一下translatesAutoresizingMaskIntoConstraints

官方文档说明

image.png

  1. 它主要用于控制是否将View的autoresizing mask转换成Auto Layout的约束

  2. 如果将这个属性的值设置为true,系统会创建一组约束,以复制视图的autoresizing mask所指定的行为

    • 也就是说:当将该属性设置为true时,除了我们自己添加的约束之外,系统还会自动添加一组约束到视图中
  3. autoresizing mask constraints完全指定了视图的大小和位置;如果要添加额外的约束来修改这个大小或位置,就会引入冲突。如果要使用AutoLayout就必须设置为false并提供约束

  4. 默认情况下,对于通过编程方式创建的任何视图,该属性被设置为true。如果在Interface Builder中添加视图,系统会自动将该属性设置为false

    • 也就是说:在Interface Builder中创建的视图(即.xib或.storyboard文件中)默认情况下该属性设置为false。在代码中创建的视图默认情况下该属性设置为true

autoresizing masks

autoresizing mask 提供了一个简单的API,用于描述子视图在其父视图大小更改时应如何响应。

换句话说,视图的autoresizing mask允许您指定当其父视图调整大小时,视图的高度、宽度、顶部、左侧、右侧和底部边距是否应该改变

可以在Xcode中调整autoresizing mask

image.png

几个问题

  1. translatesAutoresizingMaskIntoConstraints实际上是做什么的?

    • translatesAutoresizingMaskIntoConstraints是UIView的一个属性,它使开发人员能够将iOS最初使用的布局系统模拟为更近期的AutoLayout系统。
    • 为了不破坏在AutoLayout之前编写的UI代码,iOS决定在以编程方式初始化UIView时将该属性设置为true。
    • 当我们想要为UIView添加我们自己的一组AutoLayout约束时,非常重要的一点是我们必须手动将该属性设置回false,否则两组约束将会发生冲突

巧用defer处理必走的回调

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

  1. 如下Before部分的代码,如果我们少写了一个completion(xxx),代码不会报错,但实际上逻辑并不完备,有潜在的风险
  2. 而After部分的代码,完美的解决了这个问题。编译器会保证result一定有值并且只会被赋值一次,defer会在fetchData返回前最后执行一次,此时result一定是有值的
// Before
import Foundation

func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        guard error == nil else {
            completion(.failure(error!))
            return
        }

        guard let data else {
            completion(.failure(NetworkError.noData))
            return
        }

        completion(.success(data))
    }
    .resume()
}

// After
import Foundation

func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        let result: Result<Data, Error>

        defer {
            completion(result)
        }

        guard error == nil else {
            result = .failure(error!)
            return
        }

        guard let data else {
            result = .failure(NetworkError.noData)
            return
        }

        result = .success(data)
    }
    .resume()
}

多使用prepareForDisplay加载大的图片文件

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

prepareForDisplay 会在后台线程解码图片,并且解码完成准备显示时回调completionHandler 从而提升性能

import UIKit

// Before
// 直接加载大图会阻塞主线程
let image = UIImage(named: "big-image")
imageView.image = image

// After
let image = UIImage(named: "big-image")
// 使用 UIKit中回调的方式 异步加载大图
image?.prepareForDisplay { [weak self] preparedImage in
    DispatchQueue.main.async {
        self?.imageView.image = preparedImage
    }
}

// using Swift Concurrency
let image = UIImage(named: "big-image")
// 如果使用并发异步进行加载图片,加载完成后会自动切换回
Task {
    imageView.image = await image?.byPreparingForDisplay()
}

下标语法支持重载,可以用它来重写一个带有safe标签 的下标

www.swiftwithvincent.com/blog/hidden… subscript 和 方法 除了声明的关键字以及subscript没有名字外非常相似

// 声明下标语法
extension Array {
    subscript(index: Int) -> Element {
        // ...
    }
}
// 下标语法通过中括号调用
let numbers = [0, 2, 4]
numbers[2] // returns 4

// 下标语法可以有不止一个参数
extension Dictionary {
    subscript(
        key: Key,
        default defaultValue: @autoclosure () -> Value
    ) -> Value {
        // ...
    }
}

let dictionary = [ "zero": 0, "one": 1, "two": 2]
dictionary["two", default: -1] // returns 2
dictionary["three", default: -1] // returns -1

// subscript默认是没有参数标签的,我们可以添加参数标签进行重载制作一个safe方法
extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

let data = [1, 3, 4]
data[10] // will crash 💥
data[safe: 10] // returns nil

在单元测试里也可以简单测试程序性能

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

  1. 写一个测试函数,使用measure函数的闭包参数将我们的代码放进去
  2. 第一次运行时会取10次的平均值,我们可以把这个值设置为基准

image.png 3. 之后,如果该测试函数的耗时超过基准,这个单元测试就会失败

// App Code
import Foundation

func costlyFunction() -> Int {
    var result = 0
    for i in 1...2_000_000 {
        result += i
    }
    return result
}

// Test Code
import XCTest
@testable import TestPerformance

final class TestPerformanceTests: XCTestCase {
    func testPerformance() throws {
        measure(metrics: [XCTMemoryMetric(), XCTStorageMetric()]) {
            let _ = costlyFunction()
        }
    }
}

格式化日期使用yyyy,不要使用YYYY

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

依稀记得前些年刷博客刷到一个使用YYYY格式化日期导致的问题。 blog.csdn.net/shenzhou_yh…

YYYY如果最后一周是跨年的,会算作上一年从而导致错误的产生

import Foundation

let date = dateOf_01_01_2023()

let dateFormatter = DateFormatter()

dateFormatter.dateFormat = "dd/MM/YYYY"
dateFormatter.string(from: date) // 01/01/2022

dateFormatter.dateFormat = "dd/MM/yyyy"
dateFormatter.string(from: date) // 01/01/2023

5.9之后,多使用Switch和if 表达式简化代码

www.swiftwithvincent.com/blog/hidden…

从swift5.9开始,switch和if的简单语句也可以被当做表达式来使用

复杂代码

// 先声明变量及其类型,然后在每个case中给变量赋值
let comment: String 
if Int.random(in: 0...3).isMultiple(of: 2) {
    comment = "It's an even integer"
} else {
    comment = "It's an odd integer"
}

let spelledOut: String
switch Int.random(in: 0...3) {
    case 0: spelledOut = "Zero"
    case 1: spelledOut = "One"
    case 2: spelledOut = "Two"
    case 3: spelledOut = "Three"
    default: spelledOut = "Out of range"
}

简化一点代码

// 使用闭包可以省略先声明变量及其类型这一步骤,从而让编译器自动推断,然后在每个case中给变量赋值
let comment = {
    if Int.random(in: 0...3).isMultiple(of: 2) {
         return "It's an even integer"
    } else {
        return "It's an odd integer"
    }
}()

let spelledOut = {
    switch Int.random(in: 0...3) {
    case 0: return "Zero"
    case 1: return "One"
    case 2: return "Two"
    case 3: return "Three"
    default: return "Out of range"
    }
}()

最简代码

import Foundation

let comment = if Int.random(in: 0...3).isMultiple(of: 2) {
        "It's an even integer"
    } else {
        "It's an odd integer"
    }

let spelledOut = switch Int.random(in: 0...3) {
    case 0:
        "Zero"
    case 1:
        "One"
    case 2:
        "Two"
    case 3:
        "Three"
    default:
        "Out of range"
}

利用PhantomType隔离相同成员的不同类型,让问题在编译期暴露出来

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

下面的代码,handle函数中对locations调用filter,Person和Location都有一个id字段且类型相同,为了在filter中Person和Location的id不可比较,我们使用了PhantomType类型 Id<T> 作为它们各自的id类型

import Foundation

struct ID<T>: Equatable {
    private let value = UUID()
}

struct Person {
   let id = ID<Self>() // `id` is of type `ID<Person>`
   let name: String
}
struct Location {
   let id = ID<Self>() // `id` is of type `ID<Location>`
   let coordinates: (Double, Double)
}

func handle(locations: [Location]) {
    let me = Person(name: "Vincent")
    let filtered = locations.filter { $0.id == me.id }
}

定义常量时考虑使用enum 而不是将它们定义在全局命名空间中

www.swiftwithvincent.com/blog/hidden…

错误实践

一般情况下,我们定义常量时会在全局命名空间或者局部变量中

import Foundation

let baseUrl = URL(string: "https://myapi.com")
let apiPath = "/api/v2/"
let apiKey = "fskf8h99Fs7HV1jHNJF19g0268"

正确实践

将定义放到没有case 变量的enum里面,enum没有构造函数不会被外部创建实例,可以保证所有的变量仅通过enum 名访问

import Foundation

enum Constant {
    static let baseUrl = URL(string: "https://myapi.com")
    static let apiPath = "/api/v2/"
    static let apiKey = "fskf8h99Fs7HV1jHNJF19g0268"
}

多行字符串时多使用"""语法,而不是穿插\n

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

let string = """
1st line
2nd line
3rd line
4th line
"""

当你确定一个类不被继承或一个方法不被重载时,尝试使用final修饰它,从而享受编译器带来的优化

www.swiftwithvincent.com/blog/hidden…

下面的代码使用final修饰后

  1. 对firstMethod和secondMethod的调用将从动态调用转变为静态调用
  2. 提高了代码可读性。其他人读到该代码时就已经确定此为最终实现,无需一步步往上过一遍父类实现
final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
    }

    func firstMethod() {
        // ...
    }

    func secondMethod() {
        // ...
    }
}

iOS14之后,请使用UIAction替代OC Style的#selector(methodName)

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

当我们给Button添加事件的时候,会习惯性的使用OC风格的#selector,但其实有更好的方式。 使用OC风格的#selector,有以下几个问题

  1. 性能略差。OC的Method走的消息发送那一套
  2. 需要属性标注.@objc,#selector(methodName)
  3. .touchUpInside仅限于Button这类触摸屏上的控件

修改为Swift风格之后,有以下好处

  1. 性能会稍微好一些。Swift类中的方法默认走的类似虚表那一套
  2. .primaryActionTriggered甚至可以响应Apple TV上使用遥控器选中、iPad上使用键盘选中等方式。稍微有了些跨平台性
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        let action = UIAction { [weak self] _ in
            self?.buttonTapped()
        }

        button.addAction(action, for: .primaryActionTriggered)
    }

   func buttonTapped() {
       print("Button was tapped!")
    }
}

把可失败的初始化处理交给调用者

www.swiftwithvincent.com/blog/hidden…

不错的代码

假如我们有一个User类,可以通过name参数初始化,但又不希望name为空。这时候我们会选择使用可失败的构造器init?

但这样有一个问题,调用者没法知道这个User是可失败的,也就不会处理可能失败的case。

// Failable init
struct User {
    let name: String

    init?(name: String) {
        guard name.isEmpty == false else {
            return nil
        }

        self.name = name
    }
}

let user = User(name: "") // nil

更好的代码

为了让调用者在调用的时候就知道可失败,并且处理失败的原因,我们可以使用如下的代码

// Throwing init
enum UserCreationError: Error {
    case emptyName
}

struct User {
    let name: String

    init(name: String) throws {
        guard name.isEmpty == false else {
            throw UserCreationError.emptyName
        }

        self.name = name
    }
}

do {
    let user = try User(name: "")
} catch {
    print(error) // emptyName
}

使用 泛型语法配合typealias定义别名,让代码更加通用

www.swiftwithvincent.com/blog/hidden…

typealias Point<T: Numeric> = (x: T, y: T)

let pointUsingInt = Point(x: 1, y: 2) 

let pointUsingDouble = Point(x: 3.0, y: 4.0) 

let pointUsingString = Point(x: "a", y: "b") 

typealias CompletionHandler<T> = (Result<T, Error>) -> Void

func fetchData(_ completion: @escaping CompletionHandler<Data>) {
    // ...
}

使用系统API PersonNameComponentsFormatter 让代码处理更加本地化

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

import Foundation

struct User {
    let givenName: String
    let familyName: String
}

let personNameFormatter = PersonNameComponentsFormatter()

extension User {
    var fullName: String {
        var components = PersonNameComponents()
        components.givenName = givenName
        components.familyName = familyName
        return personNameFormatter.string(from: components)
    }
}

let user = User(
    givenName: "Vincent",
    familyName: "Pradeilles"
)

user.fullName // Vincent Pradeilles

let koreanLocale = Locale(identifier: "ko_KR")
personNameFormatter.locale = koreanLocale

user.fullName // Pradeilles Vincent