菜鸟教程小白 发表于 2022-12-13 05:30:30

ios - SwiftUI 在用户导航到它们之前创建目标 View


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

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

<p>为了摆脱 Apple 教程和 WWDC 视频中常见的 <code>List</code> 示例,应用需要从 REST API 获取每个屏幕的数据。</p>

<p>问题:SwiftUI 的声明式语法会导致在 <code>List</code> 中的行出现时创建所有目标 View 。 </p>

<p>这是一个使用 Stack Overflow API 的示例。第一个屏幕中的列表将显示问题列表。选择一行将导致第二个屏幕显示所选问题的正文。完整的 Xcode 项目是 <a href="https://github.com/matteom/NetworkedList" rel="noreferrer noopener nofollow">on GitHub</a> )</p>

<p>首先,我们需要一个代表问题的结构。</p>

<pre><code>struct Question: Decodable, Hashable {
    let questionId: Int
    let title: String
    let body: String?
}

struct Wrapper: Decodable {
    let items:
}
</code></pre>

<p>(需要 <code>Wrapper</code> 结构,因为 Stack Exchange API 将结果包装在 JSON 对象中)</p>

<p>然后,我们为第一个屏幕创建一个 <code>BindableObject</code>,它从 REST API 获取问题列表。</p>

<pre><code>class QuestionsData: BindableObject {
    let didChange = PassthroughSubject&lt;QuestionsData, Never&gt;()

    var questions: = [] {
      didSet { didChange.send(self) }
    }

    init() {
      let url = URL(string: &#34;https://api.stackexchange.com/2.2/questions?site=stackoverflow&#34;)!
      let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
      session.dataTask(with: url) { (data, response, error) in
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let wrapper = try! decoder.decode(Wrapper.self, from: data!)
            self?.questions = wrapper.items
      }.resume()
    }
}
</code></pre>

<p>同样,我们为详细信息屏幕创建第二个 <code>BindableObject</code>,它获取所选问题的正文(为简单起见,请原谅网络代码的重复)。</p>

<pre><code>class DetaildData: BindableObject {
    let didChange = PassthroughSubject&lt;DetaildData, Never&gt;()

    var question: Question {
      didSet { didChange.send(self) }
    }

    init(question: Question) {
      self.question = question
      let url = URL(string: &#34;https://api.stackexchange.com/2.2/questions/\(question.questionId)?site=stackoverflow&amp;filter=!9Z(-wwYGT&#34;)!
      let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
      session.dataTask(with: url) { (data, response, error) in
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let wrapper = try! decoder.decode(Wrapper.self, from: data!)
            self?.question = wrapper.items
            }.resume()
    }
}
</code></pre>

<p>两个 SwiftUIView 很简单。</p>

<ul>
<li><p>第一个在 <code>NavigationView</code> 内包含一个 <code>List</code>。每个
行包含在通向详细信息的 <code>NavigationButton</code> 中
屏幕。</p></li>
<li><p>第二个 View 只是在多行中显示问题的正文
<code>文本</code> View 。</p></li>
</ul>

<p>每个 View 都有一个 <code>@ObjectBinding</code> 到上面创建的相应对象。</p>

<pre><code>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)
      }
    }
}
</code></pre>

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

<p>但问题是每个 <code>NavigationButton</code> 都需要一个目标 View 。鉴于 SwiftUI 的声明性质,当填充列表时,会立即为每一行创建一个 <code>DetailView</code>。</p>

<p>有人可能会说 SwiftUIView 是轻量级结构,所以这不是问题。问题是这些 View 中的每一个都需要一个 <code>DetaildData</code> 实例,该实例在创建时立即启动网络请求,然后用户点击一行。您可以在其初始化程序中放置断点或 <code>print</code> 语句来验证这一点。</p>

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

<p>但这仍然会导致创建多个 <code>DetaildData</code> 实例,这些实例从未使用过,而且浪费内存。此外,在这个简单的示例中,这些对象是轻量级的,但在其他情况下,它们的构建成本可能很高。</p>

<p>这就是 SwiftUI 应该如何工作的吗?还是我遗漏了一些关键概念?</p></p>
                                    <br><hr><h1><strong>Best Answer-推荐答案</ strong></h1><br>
                                            <p><p>如您所见,当询问 <code>List</code> 时,<code>List</code>(或 <code>ForEach</code>)会为其每一行创建行 View 为它的 body 。具体来说,在这段代码中:</p>

<blockquote>
<pre><code>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)
                }
            }
      }
    }
}
</code></pre>
</blockquote>

<p>当 SwiftUI 向 <code>QuestionListView</code> 询问其 <code>body</code> 时,<code>QuestionListView</code> <code>body</code> 访问器将立即创建一个 <code> <code>data.questions</code> 中的每个 <code>Question</code> 都有一个 DetailView</code> 和一个 <code>DetailData</code>。</p>

<p><strong>然而</strong>,在 <code>DetailView</code> 在屏幕上。所以如果你的 <code>List</code> 在屏幕上有 12 行的空间,SwiftUI 只会询问前 12 个 <code>DetailView</code> 的 <code>body</code> 属性。</p >

<p>所以,不要在 <code>DetailData</code> 的 <code>init</code> 中启动 <code>dataTask</code>。在 <code>DetailData</code> 的 <code>question</code> 访问器中懒洋洋地启动它。这样,在 SwiftUI 向 <code>DetailView</code> 询问其 <code>body</code> 之前,它不会运行。</p></p>
                                   
                                                <p style="font-size: 20px;">关于ios - SwiftUI 在用户导航到它们之前创建目标 View ,我们在Stack Overflow上找到一个类似的问题:
                                                        <a href="https://stackoverflow.com/questions/56821074/" rel="noreferrer noopener nofollow" style="color: red;">
                                                                https://stackoverflow.com/questions/56821074/
                                                        </a>
                                                </p>
                                       
页: [1]
查看完整版本: ios - SwiftUI 在用户导航到它们之前创建目标 View