【发布时间】:2019-11-11 05:58:05
【问题描述】:
我很难在 SwiftUI 中创建一个在 UIKit 中非常常见的用例。
这里是场景。假设我们要创建一个主/从应用程序,用户可以在其中从列表中选择一个项目并导航到具有更多详细信息的屏幕。
为了摆脱 Apple 教程和 WWDC 视频中常见的 List 示例,应用需要从 REST API 获取每个屏幕的数据。
问题:SwiftUI 的声明式语法会导致在 List 中的行出现时创建所有目标视图。
这是一个使用 Stack Overflow API 的示例。第一个屏幕中的列表将显示问题列表。选择一行将导致第二个屏幕显示所选问题的正文。完整的 Xcode 项目是on GitHub)
首先,我们需要一个代表问题的结构。
struct Question: Decodable, Hashable {
let questionId: Int
let title: String
let body: String?
}
struct Wrapper: Decodable {
let items: [Question]
}
(需要 Wrapper 结构,因为 Stack Exchange API 将结果包装在 JSON 对象中)
然后,我们为第一个屏幕创建一个BindableObject,它从 REST API 获取问题列表。
class QuestionsData: BindableObject {
let didChange = PassthroughSubject<QuestionsData, Never>()
var questions: [Question] = [] {
didSet { didChange.send(self) }
}
init() {
let url = URL(string: "https://api.stackexchange.com/2.2/questions?site=stackoverflow")!
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
session.dataTask(with: url) { [weak self] (data, response, error) in
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let wrapper = try! decoder.decode(Wrapper.self, from: data!)
self?.questions = wrapper.items
}.resume()
}
}
同样,我们为详细信息屏幕创建了第二个BindableObject,它获取所选问题的正文(为简单起见,请原谅网络代码的重复)。
class DetaildData: BindableObject {
let didChange = PassthroughSubject<DetaildData, Never>()
var question: Question {
didSet { didChange.send(self) }
}
init(question: Question) {
self.question = question
let url = URL(string: "https://api.stackexchange.com/2.2/questions/\(question.questionId)?site=stackoverflow&filter=!9Z(-wwYGT")!
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
session.dataTask(with: url) { [weak self] (data, response, error) in
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let wrapper = try! decoder.decode(Wrapper.self, from: data!)
self?.question = wrapper.items[0]
}.resume()
}
}
两个 SwiftUI 视图很简单。
第一个在
NavigationView中包含List。每个 行包含在指向详细信息的NavigationButton中 屏幕。第二个视图只是在多行中显示问题的正文
Text查看。
每个视图都有一个@ObjectBinding 指向上面创建的相应对象。
struct QuestionListView : View {
@ObjectBinding var data: QuestionsData
var body: some View {
NavigationView {
List(data.questions.identified(by: \.self)) { question in
NavigationButton(destination: DetailView(data: DetaildData(question: question))) {
Text(question.title)
}
}
}
}
}
struct DetailView: View {
@ObjectBinding var data: DetaildData
var body: some View {
data.question.body.map {
Text($0).lineLimit(nil)
}
}
}
如果您运行该应用程序,它就可以工作。
但问题是每个NavigationButton 都想要一个目标视图。鉴于 SwiftUI 的声明性质,当填充列表时,会立即为每一行创建一个 DetailView。
有人可能会说 SwiftUI 视图是轻量级结构,所以这不是问题。问题是这些视图中的每一个都需要一个DetaildData 实例,它在创建时立即启动网络请求,然后用户点击一行。您可以在其初始化程序中放置断点或print 语句来验证这一点。
当然,可以通过将网络代码提取到单独的方法中来延迟 DetaildData 类中的网络请求,然后我们使用 onAppear(perform:) 调用该方法(您可以在 GitHub 上的最终代码中看到)。
但这仍然会导致创建多个DetaildData 实例,这些实例从未使用过,而且浪费内存。此外,在这个简单的示例中,这些对象是轻量级的,但在其他情况下,它们的构建成本可能很高。
这就是 SwiftUI 应该如何工作的吗?还是我遗漏了一些关键概念?
【问题讨论】:
-
似乎 SwiftUI 鼓励通过轻量级初始化创建两步视图,就像您在示例中所做的那样。这在概念上类似于 UITableView 和 UICollectionView 中的预取 API,因此它实际上是可取的。
标签: ios list networking swiftui navigationview