SwiftUI создает целевые представления, прежде чем пользователь перейдет к ним - PullRequest
1 голос
/ 30 июня 2019

Мне трудно создать в 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? или я упускаю какую-то критическую концепцию?

1 Ответ

1 голос
/ 30 июня 2019

Как вы обнаружили, List (или ForEach) создает представление строки для каждой из своих строк, когда у List запрашивается его тело. Конкретно в этом коде:

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 немедленно создает один DetailView и один DetailData для каждого Question в data.questions.

Однако , SwiftUI не запрашивает DetailView его body, пока DetailView не появится на экране. Таким образом, если на вашем List есть место для 12 строк на экране, SwiftUI будет запрашивать только у первых 12 DetailView s их свойства body.

Итак, не сбрасывайте dataTask в DetailData init. Начните лениво в DetailData question аксессор. Таким образом, он не будет работать, пока SwiftUI не запросит у DetailView его body.

...