SwiftUI 在用户导航到目标视图之前创建目标视图

Mat*_*ini 7 networking list ios navigationview swiftui

我很难在 SwiftUI 中创建 UIKit 中一个非常常见的用例。

这是场景。假设我们要创建一个主/从应用程序,用户可以在其中从列表中选择一个项目并导航到包含更多详细信息的屏幕。

为了摆脱ListApple 教程和 WWDC 视频中的常见示例,该应用程序需要从 REST API 获取每个屏幕的数据。

问题:SwiftUI 的声明性语法会导致所有目标视图一List出现就创建。

这是一个使用 Stack Overflow API 的示例。第一个屏幕中的列表将显示问题列表。选择一行将打开第二个屏幕,显示所选问题的正文。完整的 Xcode 项目在 GitHub 上

首先,我们需要一个表示问题的结构。

struct Question: Decodable, Hashable {
    let questionId: Int
    let title: String
    let body: String?
}

struct Wrapper: Decodable {
    let items: [Question]
}
Run Code Online (Sandbox Code Playgroud)

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

类似地,我们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()
    }
}
Run Code Online (Sandbox Code Playgroud)

两个 SwiftUI 视图很简单。

  • 第一个包含 a 的List内部NavigationView。每行都包含在NavigationButton通向详细信息屏幕的 a 中。

  • 第二个视图只是在多行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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您运行该应用程序,它就可以工作。

但问题是每个人都NavigationButton想要一个目的地视图。鉴于 SwiftUI 的声明性质,当列表被填充时,DetailView会立即为每一行创建一个。

有人可能会争辩说 SwiftUI 视图是轻量级结构,所以这不是问题。问题是这些视图中的每一个都需要一个DetaildData实例,在用户点击一行之前,该实例在创建时立即启动网络请求。您可以print在其初始值设定项中放置断点或语句来验证这一点。

当然,可以DetaildData通过将网络代码提取到单独的方法中来延迟类中的网络请求,然后我们调用 using onAppear(perform:)(您可以在 GitHub 上的最终代码中看到)。

但这仍然导致创建了多个DetaildData从未使用过的实例,并且浪费了内存。此外,在这个简单的例子中,这些对象是轻量级的,但在其他场景中,它们的构建成本可能很高。

这是 SwiftUI 应该如何工作吗?还是我错过了一些关键概念?

rob*_*off 6

正如您所发现的,当 a List(或 a ForEachList被要求提供其正文时,会为其每一行创建行视图。具体来说,在这段代码中:

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)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当 SwiftUI 请求QuestionListView它时bodyQuestionListView body访问器将立即为每个in创建一个DetailView和一个。DetailDataQuestiondata.questions

但是,SwiftUI在屏幕上显示之前不会询问DetailView它。因此,如果您在屏幕上有 12 行的空间,SwiftUI 只会询问前 12 行的属性。bodyDetailViewListDetailViewbody

所以,不要开始dataTaskinDetailDatainit。懒洋洋地在DetailDataquestion访问器中启动它。这样,它不会运行,直到 SwiftUI 要求DetailView它的body.