Мне трудно создать в SwiftUI довольно распространенный вариант использования в UIKit.
Вот сценарий. Предположим, мы хотим создать приложение master / detail, в котором пользователь может выбрать элемент из списка и перейти к экрану с более подробной информацией.
Чтобы воспользоваться общими List
примерами из учебника Apple и видео WWDC, приложению необходимо получить данные для каждого экрана из REST API.
Проблема: декларативный синтаксис SwiftUI приводит к созданию всех целевых представлений, как только появляются строки в List
.
Вот пример использования API переполнения стека. Список на первом экране покажет список вопросов. Выбор строки приведет ко второму экрану, который показывает тело выбранного вопроса. Полный проект Xcode - на 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 просты.
Первый содержит List
внутри NavigationView
. каждый
строка содержится в 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? или я упускаю какую-то критическую концепцию?