最近算是真正入门了 Swift 这门语言。

初识 Swift

现代 iOS App 的开发,首选用 Swift 进行开发。

Swift 是一门很年轻的编程语言,用于构建用户界面的 SwiftUI 更是等到 2019 年才发布,所以说生态还在慢慢发展,很多轮子也需要自己造,中文文档也相当缺失。所以在开发过程中,需要借助于 Google 的英文搜索来查找和解决问题,使用国内搜索引擎可能真的什么也搜索不到。

不过好在想要入门这门语言,还是有很多中文资料的。这里推荐 SwiftGG 作为入门。

Swift 语法上的问题在此处就不赘述了。它是一门设计比较完备的现代编程语言,所以相对脚本语言会更难以入门。但与此同时,这样的门槛也提高了程序的安全性,很多错误可以在编写代码时,通过静态代码检查获得提示。

作为 C++ 选手,Swift 很多语法特性对我来说还是很新颖的。比如结构体的计算属性。

初见 SwiftUI

打开 Xcode,新建一个应用程序项目,映入眼帘的便是右侧预览窗口中的「Hello World」。可以试试右边的检查器,看看文本的变化。点击右上角的「+」能够添加新组件。在开发过程中,Xcode 的实时预览确实相当强大。不过作为 VSCode 的老用户,快捷键上手还需要一段时间,「Editor」菜单中有许多编辑功能,可以看看它们的快捷键,比如我第一个学到的是 control+shift 进行多光标选择操作。

我还是推荐读者去看看苹果官方的教程,跟着一步步来大概就能明白这种 UI 构建的大致思路。不过 SwiftUI 作为新的 UI 框架还是太年轻了,有一些特性还是需要和老的 UIKit 结合,这里教程也有提到。

作为设计之初就考虑到响应式的界面框架,SwiftUI 生成跨 iPhone、iPad、Mac 平台的 UI 还算方便,但也需要根据各个平台进行微调。

SwiftUI 采用声明式,这也与现代用户界面框架 Flutter 一致。作为曾经的 React 选手,我对这种界面构造方式还算比较有亲切感,但上手时又发现了各种各样的问题。

在 body 属性中选择性显示

SwiftUI 显示的内容都需要写在 body 这个计算属性内部。

写 React 时,根据某个条件渲染组件,我通常为了方便直接用三目表达式返回不同的组件,也比较方便。但是在 SwiftUI 这种强类型语言环境下的条件渲染却成了一个麻烦事,因为不同的组件并非同一个类型,显然同一个函数/计算属性并能返回不同的类型,这时就需要借助 AnyView 来渲染了,它能将 UI 组件转换为同一个类型,即 AnyView

在下面的例子中,如果 condition 属性为真,则 SampleView 渲染为 ComponentA,反之渲染为 ComponentB

struct SampleView: View {
    var condition: Bool
    
    var body: some View {
        if condition {
            AnyView(ComponentA())
        } else {
            AnyView(ComponentB())
        }
    }
}

ForEach 的使用

当你渲染多个组件时,ForEach 是必不可少的。但是有时又会出现很多奇怪的报错。

看到 ForEach,理所当然就是迭代遍历一个数组。但是当你往括号里填入数组时,会提示你数组内的元素必须要是「Identifiable」的。一个类型是 Identifiable 的,意味着它一定有 id 这个属性。

比如像下面一样定义一个 Landmark (地标)结构体,想要用优雅的方式用列表显示许多地标的行组件(LandmarkRow),这个 Landmark 结构体就需要有 id 这个属性。

struct Landmark: Identifiable {
    var id: Int      // 需要 id 属性
    var name: String
}

struct LandmarkList: View {
    var landmarks: [Landmark]

    var body: some View {
        List {
            ForEach(landmarks) { landmark in
                LandmarkRow(landmark: landmark)
            }
        }
    }
}

不过这里有一个变通办法,就是数组下标访问,ForEach 的参数里面,参数的 id: \.self 记得写上,不然会出现警告「Non-constant range: argument must be an integer literal」(也不知道为什么一定要指定 id)。这样 Landmark 结构体就不需要 id 属性了。但是这样看起来没那么优雅了,但我感觉在实际开发中,没有 id 的数据可能更常见,所以这种写法可能会更多些。

struct Landmark: {
    var name: String
}

struct LandmarkList: View {
    var landmarks: [Landmark]

    var body: some View {
        List {
            ForEach(0 ..< landmarks.count, id: \.self) { index in
                LandmarkRow(landmark: landmarks[index])
            }
        }
    }
}

ForEach 还有一个奇怪地方,括号里面写闭区间是报错的。不过作为初学者,我也理解不了里面的原因,就这么记着吧。总之呢,写 ForEach 加上 id 参数总归是没错的。

ForEach(0 ... 10)             // 报错
ForEach(0 ..< 11)             // 无错误
ForEach(0 ... 10, id: \.self) // 无错误
ForEach(0 ..< 11, id: \.self) // 无错误