原文:Making real-world app with SwiftUI
05 Jun 2019
在 WWDC 大会开始一个星期以来,大家纷纷为今年的 SwiftUI 、昏暗模式、可更新的 CoreML 模型兴奋不已。在接下来的一周,我将会逐一介绍这些新玩意。首先从 SwiftUI 开始吧。SwiftUI 是苹果大家族中用来构造 APP 的全新方法。
SwiftUI 是一个声明式的基于组件的框架。你需要完全忘记曾经的 MVC 概念——在这种模型中,每个视图和模型之间都必须有一个控制器。在 SwiftUI 中只有状态的概念,同时视图继承于状态。当状态发生改变,SwiftUI 会根据改变后的状态重构 UI。苹果团队做了一件了不起的工作——它们提供了一个 SwiftUI 的完美教程。在教程中涉及到许多东西,比如布局、UIKit 的接口等等。
我会演示一个完全用 SwiftUI 编写的真实 APP 的案例。我们将编写一个能够查找 Github repos 的 APP。它有一个界面,会拥有一个文本框,用于输入查询关键;以及一个列表,用于展现搜索结果。我会假设你已经阅读过 SwiftUI 官方文档,因此我不再介绍最近本的概念,我主要会描述文档中所没有的内容。
首先编写一个 GithubService 类,用于创建查询请求以及 Repo 结构——用于描述一个 Github repository。
struct Repo: Decodable, Identifiable {
var id: Int
let name: String
let description: String
}
struct SearchResponse: Decodable {
let items: [Repo]
}
class GithubService {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared, decoder: JSONDecoder = .init()) {
self.session = session
self.decoder = decoder
}
func search(matching query: String, handler: @escaping (Result<[Repo], Error>) -> Void) {
guard
var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
else { preconditionFailure("Can't create url components...") }
urlComponents.queryItems = [
URLQueryItem(name: "q", value: query)
]
guard
let url = urlComponents.url
else { preconditionFailure("Can't create url from url components...") }
session.dataTask(with: url) { [weak self] data, _, error in
if let error = error {
handler(.failure(error))
} else {
do {
let data = data ?? Data()
let response = try self?.decoder.decode(SearchResponse.self, from: data)
handler(.success(response?.items ?? []))
} catch {
handler(.failure(error))
}
}
}.resume()
}
}
我们的 Repo 结构字段不多,但对于本例来说足够了。如果想在用 SwiftUI 创建的视图中使用这个 Repo 结构,你必须实现 Identifiable 协议。这个协议只需要你定义一个 id 属性,这个属性必须是一个 Hashable 类型。
接下来实现视图,用于表示 repos 列表中的一行。我们将在垂直分布的 stack view 中包含两个 label。
struct RepoRow: View {
let repo: Repo
var body: some View {
VStack(alignment: .leading) {
Text(repo.name)
.font(.headline)
Text(repo.description)
.font(.subheadline)
}
}
}
然后是 SearchView,用于描述整个屏幕。
struct SearchView : View {
@State private var query: String = "Swift"
@EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
query 字段用 @State 修饰。这表示这个视图继承了这个状态,当状态发生改变,SwiftUI 会重构该视图。SwiftUI 使用差异算法来判断发生了什么变化,同时只会更新对应的视图。SwiftUI 会将所有用 @State 标记的字段保存到单独的内存,只有对应的视图能够访问和修改这片内存。@State 是 Swift 新特性中的属性包装器,具体请参考这里。令人激动的地方是 $query
的使用,它表示获取一个属性包装器的引用,而不是值的引用。我们用它来双向绑定 TextField 和 query 变量。
另一个有意思的地方是 @EnvironmentObject。它属于 Environment 的一部分。你可以在 Environment 中注入想要的服务类,然后就可以在属于这个 Environment 的所有视图中使用它们。Environment 就是 SwiftUI 中的依赖注入。
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
RepoStore 类应该遵循 BindableObject 协议,该协议需要一个 didChange 属性。这样就可以在 Environment 中使用它并在它发生改变时重建视图。didChange 属性应当是一个 Publisher——它属于苹果的新响应式框架 Combine 中的内容。Publisher 的主要功能是在发生改变时通知所有订阅者。因此我们的 repos 数组才能在 didSet 方法中通知订阅者数据发生改变。当有新的值出现时,SwiftUI 会重构 ReposView。
@State 和 @EnvironmentObject 的最大不同是, @State 是只能被某个特定视图所访问,而 @EnvironmentObject 对 Environment 中的所有视图有效。但两者都可被 SwiftUI 用来在变化发生时跟踪变化并重建视图。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(frame: UIScreen.main.bounds)
let store = ReposStore(service: .init())
window.rootViewController = UIHostingController(
rootView: SearchView().environmentObject(store)
)
self.window = window
window.makeKeyAndVisible()
}
}
将指定的 Environment 用于启动 SwiftUI APP。
本周,我们介绍了一种全新的 iOS 开发方法。接下来几周我会介绍更多 WWDC 主题。请关注我的 Twitter,对本文有任何问题请问我。感谢您的阅读。