一个Vim老炮的Swift语法避坑指南

@李雨佳
2026-07-08 07:05
阅读 730

作者:老K | 北京某厂iOS技术组长 | 2024年6月某个加班的周五晚上


写在前面

先自我介绍下吧,免得你们觉得这文章又是哪个AI生成的。

我叫老K,坐标北京,在一家做电商的厂子干了快五年了,去年刚被提拔成iOS组的技术组长。说是组长,其实也就管着三四个兄弟,每天的工作就是在写代码和开会之间反复横跳。

先说个可能让很多人意外的事——我是个Vim死忠粉。没错,就是那个黑底白字、满屏快捷键的终端编辑器。Xcode?用过,但说实话,那启动速度比我早高峰挤地铁还慢,我实在受不了。平时写Swift我基本就在Neovim里搞定,配个LSP,体验其实也还行。当然,编译还是得切回Xcode,毕竟Apple那套工具链你绕不开。

每天通勤一小时,坐13号线从西二旗到国贸,来回两小时。这两小时基本就是我的学习时间,戴着AirPods听技术播客,或者在iPad上看Swift的官方文档。你别笑,这种碎片时间利用好了,一年下来真能学不少东西。

今天写这篇文章,主要是因为最近组里来了个新人,之前是写Flutter的,转Swift写得那叫一个"有味道"——代码里全是强制解包、闭包里疯狂self.xxx、枚举用得跟Java似的。我review他代码的时候,血压直接拉满,感觉回到了五年前自己刚写Swift的时候。

所以想着干脆写一篇Swift语法的东西,从基础到进阶都聊聊,算是给新人看的,也当是自己这五年的一次总结。文章里会穿插一些我踩过的坑,有些是真的付出了线上代价的,希望能帮你们少走点弯路。

对了,这篇文章的内容我也会同步到我们组内部的知识库里,算是给团队沉淀点东西。毕竟铁打的营盘流水的兵,人走了知识得留下。


基础篇:别小看这些"简单"的东西

变量和常量:let不是摆设

很多从其他语言转过来的同学,上来就var满天飞。兄弟,Swift里let才是亲儿子。

// 反例:明明不需要修改,你var个啥?
var userName = "老K"
var maxRetryCount = 3

// 正例:能用let就用let
let userName = "老K"
let maxRetryCount = 3

你可能会说,这不就是性能优化的事嘛,用let编译器能优化。没错,但更重要的是意图表达。当你看到一段代码里全是let,你一眼就知道这些东西在初始化之后不会被改,心智负担直接降一半。

我之前review代码发现一个兄弟,一个函数里20多个变量,全是var,但实际只有2个在后续被修改了。我当时就说,你这代码写得跟没加注释一样,别人看你的代码还得自己推理哪些变量会变。

Optionals:Swift的灵魂,也是新人的噩梦

说到Optional,这真的是Swift最精华的设计之一,但也是把无数新人逼疯的东西。

// 新人最爱:强制解包,一把梭
let name: String? = fetchUserName()
print(name!) // 万一name是nil呢?直接crash,线上事故就这么来的

// 稍微好点的:if let
if let name = fetchUserName() {
    print(name)
}

// 更优雅的:guard let
guard let name = fetchUserName() else {
    print("没拿到名字,提前退出")
    return
}
print(name) // 这里name已经是非Optional了,放心用

我给你们讲个真实的故事。去年双11大促前夕,我们App出了一个P0级别的bug——用户在结算页面偶现crash。排查了一圈,最后发现就是一个Optional强制解包的问题。某个接口返回的用户地址信息里,有个字段偶尔为null,但我们的代码直接!了。

那天晚上我带着两个兄弟在公司待到凌晨三点,一边改代码一边骂娘。从那以后,我定了个规矩:组里所有代码,禁止裸写!强制解包,除非你能在注释里说清楚为什么这里绝对不会是nil。

// 如果你确定一定有值,至少用这种写法,让review的人知道你是想过的
let config = Bundle.main.infoDictionary! // 注释:App的Info.plist一定存在

枚举:比你想象的强大得多

Swift的枚举跟C、Java的完全不是一回事。很多人用了两三年Swift,枚举还停留在"定义几个case"的阶段,暴殄天物。

// 带关联值的枚举,这个太有用了
enum NetworkResult<T> {
    case success(T)
    case failure(Error)
    case loading(progress: Double)
}

// 用起来
let result: NetworkResult<UserInfo> = fetchUser()
switch result {
case .success(let user):
    print("拿到用户信息: \(user.name)")
case .failure(let error):
    print("出错了: \(error.localizedDescription)")
case .loading(let progress):
    print("加载中: \(Int(progress * 100))%")
}

我们项目里有个状态机,就是用枚举+关联值实现的。订单状态、支付状态、物流状态,全用这套模式。代码清晰得不行,新来的人看一眼就知道有哪些状态、每个状态带什么数据。

还有个骚操作,枚举里可以加方法和计算属性:

enum AppEnvironment {
    case development
    case staging
    case production
    
    var baseURL: String {
        switch self {
        case .development: return "https://dev-api.example.com"
        case .staging: return "https://staging-api.example.com"
        case .production: return "https://api.example.com"
        }
    }
    
    var isDebug: Bool {
        return self != .production
    }
}

// 用的时候
let env: AppEnvironment = .production
print(env.baseURL) // "https://api.example.com"

这比到处写if-else判断环境优雅太多了。


进阶篇:开始上强度了

协议与泛型:Swift类型系统的精髓

如果说Optional是Swift的灵魂,那协议(Protocol)就是Swift的骨架。Apple的设计哲学里,协议导向编程(Protocol-Oriented Programming)是核心思想。

// 定义一个协议
protocol Cacheable {
    var cacheKey: String { get }
    var expiration: TimeInterval { get }
}

// 默认实现,通过协议扩展
extension Cacheable {
    var expiration: TimeInterval {
        return 3600 // 默认缓存1小时
    }
}

// 遵循协议
struct UserResponse: Codable, Cacheable {
    let id: Int
    let name: String
    let cacheKey: String { return "user_\(id)" }
    // expiration用默认的就行,不用写
}

struct ProductList: Codable, Cacheable {
    let products: [Product]
    let cacheKey: String { return "products" }
    var expiration: TimeInterval { return 180 } // 商品列表只缓存3分钟
}

泛型这块,很多人觉得难,其实核心就一句话:让代码不关心具体类型,但又能保证类型安全

// 一个通用的缓存管理器
class CacheManager<T: Cacheable & Codable> {
    private var storage: [String: (value: T, timestamp: Date)] = [:]
    
    func set(_ value: T) {
        storage[value.cacheKey] = (value: value, timestamp: Date())
    }
    
    func get() -> T? {
        guard let cached = storage.first(where: { $0.key == /* someKey */ "" }) else {
            return nil
        }
        // 检查是否过期
        if Date().timeIntervalSince(cached.timestamp) > /* value.expiration */ 0 {
            return nil
        }
        return cached.value
    }
}

当然,上面这个代码是简化版的,实际项目里我们的缓存层要复杂得多,涉及磁盘缓存、LRU淘汰策略什么的。但核心思路就是这样——用泛型+协议约束,写一次,到处用。

闭包与内存管理:一个不小心就内存泄漏

闭包是Swift里最容易出问题的地方,没有之一。[weak self]这个关键词,我都不知道在code review里说过多少遍了。

// 经典反面教材
class ViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 这样写,ViewController永远释放不了,内存泄漏!
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.updateUI() // 闭包强引用了self,self又持有timer,循环引用
        }
    }
    
    func updateUI() {
        // ...
    }
}

// 正确写法
class ViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            self.updateUI()
        }
    }
}

这里有个小技巧,Swift 5.3之后,在闭包里你可以直接写guard let self = self else { return },不需要再搞[weak self]然后self?.xxx了。这个语法糖真的很甜,省了不少代码。

但要注意,不是所有闭包都需要[weak self]。如果闭包是一次性的,比如网络请求的completion handler,通常不会有循环引用的问题。只有当闭包被长期持有(比如赋值给属性、传给Timer、加到通知中心)的时候,才需要特别注意。

// 这种一般不需要 [weak self],因为网络请求结束后闭包就释放了
func fetchUserData() {
    networkManager.request("/user/info") { [weak self] result in
        // 加个weak self也无妨,算是个好习惯
        guard let self = self else { return }
        switch result {
        case .success(let data):
            self.updateUI(with: data)
        case .failure(let error):
            self.showError(error)
        }
    }
}

值类型 vs 引用类型:搞不懂这个,Bug找上门

Swift里struct是值类型,class是引用类型。这个区别看着简单,但实际开发中很多人搞不清楚,写出各种诡异的bug。

// 值类型:赋值是拷贝
struct Point {
    var x: Double
    var y: Double
}

var p1 = Point(x: 1, y: 2)
var p2 = p1 // 拷贝了一份
p2.x = 10
print(p1.x) // 还是1,p1和p2互不影响

// 引用类型:赋值是共享
class PointClass {
    var x: Double
    var y: Double
    init(x: Double, y: Double) {
        self.x = x
        self.y = y
    }
}

var pc1 = PointClass(x: 1, y: 2)
var pc2 = pc1 // 共享同一个对象
pc2.x = 10
print(pc1.x) // 变成10了!pc1和pc2指向同一个对象

Apple官方建议是:默认用struct,只有在你确实需要引用语义(比如共享状态、继承)的时候才用class

我们项目里,数据模型(Model)全部用struct,只有ViewController、Manager这种需要共享状态的才用class。这样有个好处——struct天然线程安全(因为每次都是拷贝),在多线程场景下省心很多。

不过struct有个坑要注意,就是mutating方法:

struct Counter {
    var count: Int = 0
    
    // 修改自身属性,必须加mutating
    mutating func increment() {
        count += 1
    }
}

let counter = Counter()
// counter.increment() // 编译报错!let声明的不能调用mutating方法
var mutableCounter = Counter()
mutableCounter.increment() // OK

这个坑我见新人踩过好几次了。记住,let声明的struct实例,所有属性都是不可变的,包括调用mutating方法。


高级篇:写出优雅的Swift代码

Property Wrapper:代码复用的黑科技

Property Wrapper是Swift 5.1引入的特性,用好了能省掉大量重复代码。

// 比如我们想给所有UserDefaults的属性加个默认值
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

// 用起来超级爽
struct AppSettings {
    @UserDefault(key: "is_dark_mode", defaultValue: false)
    static var isDarkMode: Bool
    
    @UserDefault(key: "font_size", defaultValue: 14.0)
    static var fontSize: Double
    
    @UserDefault(key: "username", defaultValue: "")
    static var username: String
}

// 使用
AppSettings.isDarkMode = true // 自动存到UserDefaults
print(AppSettings.fontSize) // 14.0

我们项目里还用Property Wrapper做了线程安全包装、数据校验、日志记录等等。这东西一旦用起来,真的回不去了。

Result Builder:DSL的基石

SwiftUI的body属性为什么能写得那么优雅?靠的就是Result Builder(之前叫Function Builder)。

// 一个简单的HTML构建器
@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: "\n")
    }
    
    static func buildOptional(_ component: String?) -> String {
        return component ?? ""
    }
    
    static func buildEither(first component: String) -> String {
        return component
    }
    
    static func buildEither(second component: String) -> String {
        return component
    }
}

func div(@HTMLBuilder content: () -> String) -> String {
    return "<div>\(content())</div>"
}

func p(_ text: String) -> String {
    return "<p>\(text)</p>"
}

// 用起来
let html = div {
    p("Hello")
    p("World")
}
// 结果: <div><p>Hello</p>\n<p>World</p></div>

理解了Result Builder,你就理解了SwiftUI的声明式语法到底是怎么回事。这也是为什么我建议每个写Swift的人都要去了解一下SwiftUI,哪怕你主力还是用UIKit。

并发编程:async/await改变了游戏规则

iOS 15之后,async/await终于来了。说实话,等了太久了。以前写异步代码,要么嵌套回调(Callback Hell),要么用Combine,学习曲线都不低。

// 以前:回调地狱
func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
    // ...
}

func fetchOrders(userId: Int, completion: @escaping (Result<[Order], Error>) -> Void) {
    // ...
}

// 调用
fetchUser { result in
    switch result {
    case .success(let user):
        fetchOrders(userId: user.id) { orderResult in
            switch orderResult {
            case .success(let orders):
                // 终于拿到了,但代码已经嵌套了三层
                print(orders)
            case .failure(let error):
                print(error)
            }
        }
    case .failure(let error):
        print(error)
    }
}

// 现在:async/await,清爽!
func fetchUser() async throws -> User {
    // ...
}

func fetchOrders(userId: Int) async throws -> [Order] {
    // ...
}

// 调用
Task {
    do {
        let user = try await fetchUser()
        let orders = try await fetchOrders(userId: user.id)
        print(orders)
    } catch {
        print(error)
    }
}

但async/await也有坑。最常见的就是在UIKit的回调里用Task,一不小心就会在主线程之外更新UI:

// 坑!
button.addAction(UIAction { _ in
    Task {
        let data = try await fetchData()
        // 这里可能不在主线程!
        self.label.text = data.title // 可能crash或者UI不更新
    }
}, for: .touchUpInside)

// 正确写法
button.addAction(UIAction { _ in
    Task { @MainActor in
        let data = try await fetchData()
        // 现在一定在主线程了
        self.label.text = data.title
    }
}, for: .touchUpInside)

或者更推荐的做法,把整个ViewController标记为@MainActor

@MainActor
class ViewController: UIViewController {
    // 这个类里的所有方法默认都在主线程执行
    // 省心
}

一些实战中的小Tips

最后分享几个我这些年总结的小技巧,都是血泪教训:

1. 用enum做命名空间

enum API {
    enum User {
        static let info = "/user/info"
        static let orders = "/user/orders"
    }
    enum Product {
        static let list = "/product/list"
        static let detail = "/product/detail"
    }
}

// 使用
let url = API.User.info

2. 善用defer

func processFile(path: String) {
    let file = openFile(path)
    defer {
        closeFile(file) // 不管函数怎么退出,都会执行这行
    }
    
    // 处理文件...
    // 中间return、throw都没关系,defer保证资源释放
}

3. 用typealias让代码更易读

typealias CompletionHandler<T> = (Result<T, NetworkError>) -> Void
typealias UserID = String
typealias Timestamp = TimeInterval

// 比直接写类型清晰多了
func fetchUser(id: UserID, completion: CompletionHandler<UserInfo>) {
    // ...
}

关于SwiftUI和App Store审核的一些碎碎念

既然聊Swift,不得不提SwiftUI。虽然我们现在主力项目还是UIKit,但新项目已经在全面拥抱SwiftUI了。

说几个SwiftUI的最佳实践:

  • 状态管理要克制:不要什么都用@State,小范围的状态用@State,跨视图共享用@EnvironmentObject,别搞反了
  • 拆分子视图:SwiftUI的body如果超过50行,就该考虑拆分了。不然编译速度慢到你怀疑人生
  • Preview是你的好朋友:善用#Preview宏,能大幅提升开发效率

至于App Store审核,这又是一门玄学了。我们去年有个版本被拒了三次,原因分别是:

  1. 隐私协议里少声明了一个权限用途(NSUserTrackingUsageDescription
  2. App里有个隐藏功能没在审核备注里说明(其实是个测试入口,忘记关了)
  3. 引导用户去外部评分(Apple对这块管得很严)

所以每次提审前,我都会过一遍Apple的审核指南,特别是最近更新的条款。这块真的不能偷懒,被拒一次至少耽误两三天,赶上急的deadline能把人逼疯。


最后说两句

写了这么多,其实Swift的语法远不止这些。什么宏(Macro)、分布式Actor、Observation框架等等,都是这两年的新特性,我自己也还在学习。

但我觉得,学语言最重要的不是记住多少语法,而是理解它的设计哲学。Swift的设计哲学是什么?安全、简洁、表达力强。你写的每一行代码,都应该朝着这个方向靠拢。

好了,不说了,产品经理又催需求了,说是下周一要看到新版本的原型。每次听到这话我就头大。

对了,这篇文章的内容我已经整理到团队的知识库里了,包括一些代码片段和踩坑记录,组里的同学可以直接去翻。如果你也是刚学Swift,建议先把基础篇的东西吃透,进阶篇的慢慢来,高级篇的等你项目里真用到了再深入也不迟。

我们组的知识库地址就不贴了,毕竟是企业内部的。但如果你有什么问题,欢迎在评论区留言,我看到都会回。

最后,希望这篇文章对你有帮助。写技术文章这事儿,说白了就是把自己踩过的坑分享出来,让别人少踩几个。如果这篇文章能帮到哪怕一个人,那今晚这班加得也算值了。


老K | 2024.06 | 写于北京某厂工位

P.S. 如果你也是Vim党,欢迎来找我交流配置。我的.config/nvim配置在组内知识库也有分享,Swift开发体验还不错。

评论 0

最热最新
暂无评论
@李雨佳Lv.1
0
影响力
0
文章
0
粉丝