В SwiftUI как узнать, когда выбор Picker был изменен? Почему не работает Set? - PullRequest
1 голос
/ 06 ноября 2019

У меня есть Picker в SwiftUI Form, и я пытаюсь обработать значение всякий раз, когда изменяется сборщик. Я ожидал, что смогу сделать это с didSet для переменной, которая представляет текущее выбранное значение.

import SwiftUI

struct ContentView: View {

    enum TransmissionType: Int {
        case automatic
        case manual
    }

    @State private var selectedTransmissionType: Int = TransmissionType.automatic.rawValue {
        didSet {
            print("selection changed to \(selectedTransmissionType)")
        }
    }

    private let transmissionTypes: [String] = ["Automatic", "Manual"]

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $selectedTransmissionType,
                           label: Text("Transmission Type")) {
                        ForEach(0 ..< transmissionTypes.count) {
                            Text(self.transmissionTypes[$0])
                        }
                    }
                }
            }
        }
    }
}

Пользовательский интерфейс Picker работает (в основном), как и ожидалось: я вижу значение по умолчанию:выбрав, нажмите в палитре, он открывает новый вид, я могу выбрать другое значение, а затем он возвращается к основной форме и показывает, что выбрано новое значение. Однако didSet никогда не вызывается.

Я видел этот вопрос , но мне кажется странным добавлять больше в мой код View вместо простой обработки нового значения, когдапеременные изменения, если это вообще возможно. Лучше использовать onReceive, хотя это приводит к более сложному представлению? Мой главный вопрос: Что не так с моим кодом для предотвращения вызова didSet?

Я использовал этот пример , чтобы добраться до этой точки.

Помимо моего обычного вопроса, у меня есть несколько других об этом примере:

A) Кажется странным, как у меня есть enum и также Array для представленияте же два значения. Может ли кто-то также предложить лучший способ структурировать его, чтобы избежать этой избыточности? Я считал TransmissionType объект, но это выглядело как излишнее по сравнению с enum ... может быть, это не так?

B) При нажатии на сборщик экран спараметры выбора скользят, а затем два параметра немного подпрыгивают. Это вызывает резкий скачок и плохой пользовательский опыт. Я делаю что-то не так, что вызывает плохой UX? Или это, вероятно, ошибка SwiftUI? Я получаю эту ошибку каждый раз, когда меняю сборщик:

[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.

Ответы [ 3 ]

1 голос
/ 06 ноября 2019

Здесь есть две проблемы.

1) прыжки могут быть решены, если стиль заголовка ".inline".

2) OnReceive () - это один из самых простых способов заменить запрос didSet на объединенный каркасный метод, который является основной технологией SwiftUI.

struct ContentView: View {



enum TransmissionType: Int {
    case automatic
    case manual
}

 @State private var selectedTransmissionType: Int =   TransmissionType.automatic.rawValue {
    didSet {
        print("selection changed to \(selectedTransmissionType)")
    }
}

private let transmissionTypes: [String] = ["Automatic", "Manual"]

var body: some View {
    NavigationView {
        Form {
            Section {
                Picker(selection: $selectedTransmissionType,
                       label: Text("Transmission Type")) {
                    ForEach(0 ..< transmissionTypes.count) {
                        Text(self.transmissionTypes[$0])
                    }
                }
            }
        }.navigationBarTitle("Title", displayMode: .inline) // this solves jumping and all warnings.
    }.onReceive(Just(selectedTransmissionType)) { value in
    print(value) // Just one step can monitor the @state value.
    }
   }


}
1 голос
/ 06 ноября 2019

Я начал печатать это раньше и вернулся, чтобы найти LuLuGaGa избил меня до удара. : D Но так как у меня это все равно ...

Главный вопрос: Из Swift Language Guide :

"Когда выприсвойте значение по умолчанию хранимому свойству или установите его начальное значение в инициализаторе, значение этого свойства устанавливается напрямую, без вызова каких-либо наблюдателей свойства. "

Таким образом, наблюдатель свойства не будет запускатьсякогда представление построено. Но когда переменная @State изменяется, создается новый экземпляр представления (помните, что представления являются структурами или типами значений). Таким образом, наблюдатель свойства didSet практически не используется в свойствах @State.

То, что вы хотите сделать, - это создать класс, соответствующий ObservableObject, и ссылаться на него с вашей точки зрения с помощью@ObservedObject оболочка свойства. Поскольку класс существует вне структуры, вы можете установить свойства наблюдателей для его свойств, и они будут срабатывать так, как вы ожидаете.

Вопрос A: Вы можете использовать только перечисление, если вызаставить его соответствовать CaseIterable (см. пример ниже)

Вопрос B: Это похоже на ошибку SwiftUI, как это происходит с любым Picker внутри NavigationView /Form комбо, насколько я могу судить. Я бы порекомендовал сообщить об этом Apple .

Вот как я могу удалить избыточность перечисления и массива и сохранить выбор в UserDefaults:

extension ContentView {
    // CaseIterable lets us use .allCases property in ForEach
    enum TransmissionType: String, CaseIterable, Identifiable, CustomStringConvertible {
        case automatic
        case manual

        // This lets us omit 'id' parameter in ForEach
        var id: TransmissionType {
            self
        }

        // This just capitalizes the first letter for prettier printing
        var description: String {
            rawValue.prefix(1).uppercased() + rawValue.dropFirst()
        }
    }

    class SelectionModel: ObservableObject {
        // Save selected type to UserDefaults on change
        @Published var selectedTransmissionType: TransmissionType {
            didSet {
                UserDefaults.standard.set(selectedTransmissionType.rawValue, forKey: "TransmissionType")
            }
        }

        // Load selected type from UserDefaults on initialization
        init() {
            if let rawValue = UserDefaults.standard.string(forKey: "TransmissionType") {
                if let transmissionType = TransmissionType(rawValue: rawValue) {
                    self.selectedTransmissionType = transmissionType
                    return
                }
            }
            // Couldn't load from UserDefaults
            self.selectedTransmissionType = .automatic
        }
    }
}

Тогда ваш взгляд просто выглядит как

struct ContentView: View {
    @ObservedObject var model = SelectionModel()

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $model.selectedTransmissionType, label: Text("Transmission Type")) {
                        ForEach(TransmissionType.allCases) { type in
                            Text(type.description)
                        }
                    }
                }
            }
        }
    }
}
1 голос
/ 06 ноября 2019

didSet не вызывается в @State, потому что это оболочка - вы устанавливаете значение в пределах состояния, а не само состояние. Важный вопрос: почему вы хотите знать, что он установлен?

Ваше представление может быть немного упрощено:

Если вы объявляете свой enum как необработанный тип String, вы нене нужен ваш массив имен. Если вы объявите его как CaseIterable, вы сможете получить все случаи, вызвав Array (TransmissionType.allCases). Если он также объявлен как опознаваемый, вы сможете передать все случаи прямо в ForEach. Затем вам нужно передать rawValue в текст и не забудьте разместить на нем тег, чтобы можно было сделать выбор:

struct ContentView: View {

    enum TransmissionType: String, CaseIterable, Identifiable {

        case automatic
        case manual

        var id: String {
            return self.rawValue
        }
    }

    @State private var selectedTransmissionType = TransmissionType.automatic

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $selectedTransmissionType,
                           label: Text("Transmission Type")) {
                            ForEach(Array(TransmissionType.allCases)) {
                                Text($0.rawValue).tag($0)
                            }
                    }
                }
            }
        }
    }
}

Я не вижу, откуда происходят странные прыжки - вы реплицировали? это на устройстве, а?

...