Поток данных SwiftUI и обновление пользовательского интерфейса для объекта внешней модели с изменяющимися значениями, но тем же идентификатором - PullRequest
0 голосов
/ 10 июля 2020

У меня есть массив объектов («вещей») какой-то сторонней библиотеки, которые я хочу отобразить в представлении SwiftUI. Эти объекты "Вещи" можно идентифицировать и хэшировать по идентификатору, но при перезагрузке нового набора вещей их содержимое могло измениться (скажем, "статус" или "текст" этой Вещи, хотя это снова та же Вещь. ). Таким образом, идентификатор остается прежним, но содержание вещи может измениться. Проблема в том, что SwiftUI не обновляет пользовательский интерфейс, когда я получаю новый массив вещей. Я предполагаю, что это потому, что Вещи снова «идентифицируются» как одни и те же Вещи по их идентификатору. Я не могу изменить Вещь, потому что она из сторонней библиотеки.

Теперь я просто обернул Вещь в другой класс, и вдруг она заработала! Но я хочу понять , почему это работает, и если это определенное поведение, а не просто совпадение или «удача».

Кто-нибудь может объяснить, что здесь происходит за кулисами? В частности, в чем основное различие между DirectThingView и WrappedThingView, из-за которого SwiftUI обновляет пользовательский интерфейс для последнего, а не для первого?

Или есть какие-то предложения, как решить эту проблему лучше?

Вот пример кода, который показывает все: он отображает вещи в двух столбцах; первый столбец использует DirectThingView, а второй столбец использует WrappedThingView. Если вы нажмете кнопку «Обновить», массив things заполнится измененными объектами, но только пользовательский интерфейс правого столбца правильно обновляет значения; левый столбец всегда остается в исходном состоянии.

//
//  TestView.swift
//
//  Created by Manfred Schwind on 10.07.20.
//  Copyright © 2020 mani.de. All rights reserved.
//

import SwiftUI

// The main model contains an array of "Things",
// every Thing has an id and contains a text.
// For testing purposes, every other time a Thing gets instantiated, its text contains either "A" or "B".
// Problem here: the "current" text of a Thing with the same id can change, when Things are reloaded.
class TestViewModel: ObservableObject {
    @Published var things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
}

struct TestView: View {
    @ObservedObject var viewModel = TestViewModel()
    var body: some View {
        VStack (spacing: 30) {
            HStack (spacing: 40) {
                // We try to display the current Thing array in the UI

                // The views in the first column directly store the Thing:
                // Problem here: the UI does not update for changed Things ...
                VStack {
                    Text("Direct")
                    ForEach(self.viewModel.things, id: \.self) { thing in
                        DirectThingView(viewModel: thing)
                    }
                }
                // The views in the second column store the Thin wrapped into another class:
                // In this case, the problem magically went away!
                VStack {
                    Text("Wrapped")
                    ForEach(self.viewModel.things, id: \.self) { thing in
                        WrappedThingView(viewModel: thing)
                    }
                }
            }
            Button(action: {
                // change the Thing array in the TestViewModel, this causes the UI to update:
                self.viewModel.things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
            }) {
                Text("Reload")
            }
        }
    }
}

struct DirectThingView: View {
    // first approach just stores the passed Thing directly internally:
    private let viewModel: Thing

    init(viewModel: Thing) {
        self.viewModel = viewModel
    }

    var body: some View {
        Text(self.viewModel.text)
    }
}

struct WrappedThingView: View {
    // second approach stores the passed Thing wrapped into another Object internally:
    private let viewModel: WrappedThing

    init(viewModel: Thing) {
        // take the Thing like in the first approach, but internally store it wrapped:
        self.viewModel = WrappedThing(childModel: viewModel)
    }

    var body: some View {
        Text(self.viewModel.childModel.text)
    }

    // If type of WrappedThing is changed from class to struct, then the problem returns!
    private class WrappedThing {
        let childModel: Thing
        init(childModel: Thing) {
            self.childModel = childModel
        }
    }

}

// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable {

    // Identifiable:
    let id: Int

    // The text contains either "A" or "B", in alternating order on every new Thing instantiation
    var text: String

    init(id: Int) {
        self.id = id
        struct Holder {
            static var flip: Bool = false
        }
        self.text = Holder.flip ? "B" : "A"
        Holder.flip = !Holder.flip
    }

    // Hashable:
    public func hash(into hasher: inout Hasher) {
        hasher.combine(self.id)
    }

    // Equatable (part of Hashable):
    public static func == (lhs: Thing, rhs: Thing) -> Bool {
        return lhs.id == rhs.id
    }
}

#if DEBUG
struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}
#endif

введите описание изображения здесь

Заранее большое спасибо!

1 Ответ

0 голосов
/ 10 июля 2020

Я нашел для себя ответ. Проблема заключается в реализации Equatable of Thing здесь. Как реализовано выше, старая и новая версии одной и той же вещи (но с разным содержанием) считаются равными. Но SwiftUI различает «идентичность» и «равенство», и это должно быть правильно реализовано.

В приведенном выше коде Identifiable и Hashable в порядке, но Equatable нужно изменить, чтобы быть более точным. Так, например, это решает проблему:

// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable {

    // Identifiable:
    let id: Int

    // The text contains either "A" or "B", in alternating order on every new Thing instantiation
    var text: String

    init(id: Int) {
        self.id = id
        struct Holder {
            static var flip: Bool = false
        }
        self.text = Holder.flip ? "B" : "A"
        Holder.flip = !Holder.flip
    }

    // Hashable:
    public func hash(into hasher: inout Hasher) {
        // we are lazy and just use the id here, "collisions" are then separated by func ==
        hasher.combine(self.id)
    }

    // Equatable (part of Hashable):
    public static func == (lhs: Thing, rhs: Thing) -> Bool {
        // We are lazy again (in reality Thing has many properties) and we consider
        // two Things to be the equal ONLY when they point to the same address.
        // So we get the "same but different" semantic that we want, when we are
        // getting a new version of the Thing.
        // (Same in the sense of identity, different in the sense of equality)
        return lhs === rhs
    }
}
...