我很难在 SwiftUI 中创建一个在 UIKit 中非常常见的用例。
这里是场景。假设我们要创建一个主/从应用程序,用户可以在其中从列表中选择一个项目并导航到具有更多详细信息的屏幕。
为了摆脱 Apple 教程和 WWDC 视频中常见的 List 示例,应用需要从 REST API 获取每个屏幕的数据。
问题:SwiftUI 的声明式语法会导致在 List 中的行出现时创建所有目标 View 。
这是一个使用 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 View 很简单。
每个 View 都有一个 @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 都需要一个目标 View 。鉴于 SwiftUI 的声明性质,当填充列表时,会立即为每一行创建一个 DetailView 。
有人可能会说 SwiftUI View 是轻量级结构,所以这不是问题。问题是这些 View 中的每一个都需要一个 DetaildData 实例,该实例在创建时立即启动网络请求,然后用户点击一行。您可以在其初始化程序中放置断点或 print 语句来验证这一点。
当然,可以通过将网络代码提取到一个单独的方法中来延迟 DetaildData 类中的网络请求,然后我们使用 onAppear(perform (您可以在 GitHub 上的最终代码中看到)。
但这仍然会导致创建多个 DetaildData 实例,这些实例从未使用过,而且浪费内存。此外,在这个简单的示例中,这些对象是轻量级的,但在其他情况下,它们的构建成本可能很高。
这就是 SwiftUI 应该如何工作的吗?还是我遗漏了一些关键概念?
Best Answer-推荐答案 strong>
如您所见,当询问 List 时,List (或 ForEach )会为其每一行创建行 View 为它的 body 。具体来说,在这段代码中:
struct QuestionListView : View {
@ObjectBinding var data: QuestionsData
var body: some View {
NavigationView {
List(data.questions.identified(by: \.self)) { question in
NavigationButton(destination: DetailView(data: DetailData(question: question))) {
Text(question.title)
}
}
}
}
}
当 SwiftUI 向 QuestionListView 询问其 body 时,QuestionListView body 访问器将立即创建一个 data.questions 中的每个 Question 都有一个 DetailView 和一个 DetailData 。
然而,在 DetailView 在屏幕上。所以如果你的 List 在屏幕上有 12 行的空间,SwiftUI 只会询问前 12 个 DetailView 的 body 属性。
所以,不要在 DetailData 的 init 中启动 dataTask 。在 DetailData 的 question 访问器中懒洋洋地启动它。这样,在 SwiftUI 向 DetailView 询问其 body 之前,它不会运行。
关于ios - SwiftUI 在用户导航到它们之前创建目标 View ,我们在Stack Overflow上找到一个类似的问题:
https://stackoverflow.com/questions/56821074/
|