Обработка производного состояния в SwiftUI - PullRequest
3 голосов
/ 24 января 2020

говорят, что я создаю представление «Редактор даты». Цель состоит в том, чтобы: - взять по умолчанию начальную дату. - Это позволяет пользователю изменять ввод. - Если пользователь затем выбирает, он может нажать «Сохранить», и в этом случае владелец представления может решить что-то сделать с данными.

Вот один из способов его реализации:

struct AlarmEditor : View {
    var seedDate : Date
    var handleSave : (Date) -> Void

    @State var editingDate : Date?

    var body : some View {
        let dateBinding : Binding<Date> = Binding(
            get: {
                return self.editingDate ?? seedDate
            },
            set: { date in
                self.editingDate = date
            }
        )

        return VStack {
            DatePicker(
                selection: dateBinding,
                displayedComponents: .hourAndMinute,
                label: { Text("Date") }
            )
            Spacer()
            Button(action: {
                self.handleSave(dateBinding.wrappedValue)
            }) {
                Text("Save").font(.headline).bold()
            }
        }
    }
}

Проблема

Что если владелец изменит значение seedDate?

Скажем, в этом случае я хотел сбросить значение editingDate до новой seedDate.

Каким будет идиоматический c способ сделать это?

Ответы [ 4 ]

4 голосов
/ 04 февраля 2020

Я бы предпочел сделать это через явно используемый ViewModel для такого редактора, и это требует минимальных изменений в вашем коде. Вот возможный подход (проверено и работало с Xcode 11.2.1):

Тестирование родителя

struct TestAlarmEditor: View {
    private var editorModel = AlarmEditorViewModel()
    var body: some View {
        VStack {
            AlarmEditor(viewModel: self.editorModel, handleSave: {_ in }, editingDate: nil)
            Button("Reset") {
                self.editorModel.seedDate = Date(timeIntervalSinceNow: 60 * 60)
            }
        }
    }
}

Простая модель представления для редактора

class AlarmEditorViewModel: ObservableObject {
    @Published var seedDate = Date() // << can be any or set via init
}

Обновленный редактор

struct AlarmEditor : View {
    @ObservedObject var viewModel : AlarmEditorViewModel

    var handleSave : (Date) -> Void

    @State var editingDate : Date?

    var body : some View {
        let dateBinding : Binding<Date> = Binding(
            get: {
                return self.editingDate ?? self.viewModel.seedDate
            },
            set: { date in
                self.editingDate = date
            }
        )

        return VStack {
            DatePicker(
                selection: dateBinding,
                displayedComponents: .hourAndMinute,
                label: { Text("Date") }
            )
            .onReceive(self.viewModel.$seedDate, perform: { 
                self.editingDate = $0 })                    // << reset here
            Spacer()
            Button(action: {
                self.handleSave(dateBinding.wrappedValue)
            }) {
                Text("Save").font(.headline).bold()
            }
        }
    }
}
3 голосов
/ 04 февраля 2020

Я не уверен, что понял цель seedDate здесь. Но я думаю, что вы полагаетесь на события (что-то вроде UIKit) слишком сильно вместо принципа единственного источника истины (способ SwiftUI).

Обновление : Добавлен способ отмены даты издания. В этом случае представление редактора должно изменять Binding только при сохранении. Для этого он использует приватный State, который будет использоваться для выбора даты. Таким образом, источник истины сохраняется, так как используемое частное состояние никогда не покинет контекст представления редактирования.

struct ContentView: View {
    @State var dateEditorVisible = false
    @State var date: Date = Date() // source of truth

    var body: some View {
        NavigationView {
            VStack {
                Text("\(date.format("HH:mm:ss"))")

                Button(action: self.showDateEditor) {
                    Text("Edit")
                }
                .sheet(isPresented: $dateEditorVisible) {
                    // Here we provide a two way binding to the `date` state
                    // and a way to dismiss the editor view.
                    DateEditorView(date: self.$date, dismiss: self.hideDateEditor)
                }
            }
        }
    }

    func showDateEditor() {
        dateEditorVisible = true
    }

    func hideDateEditor() {
        dateEditorVisible = false
    }
}
struct DateEditorView: View {
    // Only a binding.
    // Updating this value will update the `@State date` of the parent view
    @Binding var date: Date

    @State private var editingDate: Date = Date()
    private var dismiss: () -> Void

    init(date: Binding<Date>, dismiss: @escaping () -> Void) {
        self._date = date
        self.dismiss = dismiss

        // assign the wrapped value as default value for edition
        self.editingDate = date.wrappedValue
    }

    var body: some View {
        VStack {
            DatePicker(selection: $editingDate, displayedComponents: .hourAndMinute) {
                Text("Date")
            }

            HStack {
                Button(action: self.save) {
                    Text("Save")
                }

                Button(action: self.dismiss) {
                    Text("Cancel")
                }
            }
        }
    }

    func save() {
        date = editingDate
        dismiss()
    }
}

Таким образом, вам не нужно определять действие сохранения обновить родительское представление или сохранить в синхронизации c текущее значение с некоторым значением по умолчанию. У вас есть только единственный источник истины , который управляет всем вашим пользовательским интерфейсом.

Редактировать :

Расширение Date для его построения .

extension Date {
    private static let formater = DateFormatter()

    func format(_ format: String) -> String {
        Self.formater.dateFormat = format
        return Self.formater.string(from: self)
    }
}
2 голосов
/ 10 февраля 2020

Комментарий и предупреждение

По сути, этот вопрос составляет в поисках замены для didSet в операциях var seedDate.

Я использовал один из своих запросы поддержки с Apple по этому же вопросу несколько месяцев go. Последний ответ от них состоял в том, что они получили несколько таких вопросов, но у них пока нет «хорошего» решения. Я поделился решением, приведенным ниже, и они ответили: «Поскольку это работает, используйте его».

То, что следует ниже, довольно «вонючее», но оно работает. Надеемся, мы увидим улучшения в iOS 14, устраняющие необходимость чего-то подобного.

Концепция

Мы можем воспользоваться тем, что body является единственной точкой входа для просмотр рендеринга. Таким образом, мы можем отслеживать изменения входов нашего представления с течением времени и изменять внутреннее состояние, основываясь на этом. Мы просто должны быть осторожны с тем, как мы обновляем вещи, чтобы идея SwiftUI о State не изменялась неправильно.

Мы можем сделать это, используя struct, который содержит два ссылочных значения:

  1. Значение, которое мы хотим отслеживать
  2. Значение, которое мы хотим изменить при изменении # 1

Если мы хотим, чтобы SwiftUI обновлялся, мы заменяем эталонное значение. Если мы хотим обновить на основе изменений # 1 внутри тела, мы обновляем значение , удерживаемое опорным значением.

Реализация

Суть здесь

Сначала мы хотим обернуть любое значение в ссылочный тип. Это позволяет нам сохранять значение без запуска механизмов обновления SwiftUI.

// A class that lets us wrap any value in a reference type
class ValueHolder<Value> {
    init(_ value: Value) { self.value = value }
    var value: Value
}

Теперь, если мы объявим @State var valueHolder = ValueHolder(0), мы можем сделать:

Button("Tap me") {
  self.valueHolder.value = 0 // **Doesn't** trigger SwiftUI update
  self.valueHolder = ValueHolder(0) // **Does** trigger SwiftUI update
}

Во-вторых, создайте оболочку свойства, которая содержит два из них, один для нашего внешнего входного значения и один для нашего внутреннего состояния.

См. этот ответ для объяснения того, почему я использую State в оболочке свойства.

// A property wrapper that holds a tracked value, and a value we'd like to update when that value changes.
@propertyWrapper
struct TrackedValue<Tracked, Value>: DynamicProperty {
    var trackedHolder: State<ValueHolder<Tracked>>
    var valueHolder: State<ValueHolder<Value>>

    init(wrappedValue value: Value, tracked: Tracked) {
        self.trackedHolder = State(initialValue: ValueHolder(tracked))
        self.valueHolder = State(initialValue: ValueHolder(value))
    }

    var wrappedValue: Value {
        get { self.valueHolder.wrappedValue.value }
        nonmutating set { self.valueHolder.wrappedValue = ValueHolder(newValue) }
    }

    var projectedValue: Self { return self }
}

И, наконец, добавьте удобный метод, который позволит нам эффективно обновлять информацию по мере необходимости. Так как это возвращает View, вы можете использовать его внутри любого ViewBuilder.

extension TrackedValue {
    @discardableResult
    public func update(tracked: Tracked, with block:(Tracked, Value) -> Value) -> some View {
        self.valueHolder.wrappedValue.value = block(self.trackedHolder.wrappedValue.value, self.valueHolder.wrappedValue.value)
        self.trackedHolder.wrappedValue.value = tracked
        return EmptyView()
    }
}

Использование

Если вы запустите приведенный ниже код, childCount будет сбрасываться в 0 каждый раз masterCount изменения.

struct ContentView: View {
    @State var count: Int = 0

    var body: some View {
        VStack {
            Button("Master Count: \(self.count)") {
                self.count += 1
            }
            ChildView(masterCount: self.count)
        }
    }
}

struct ChildView: View {
    var masterCount: Int
    @TrackedValue(tracked: 0) var childCount: Int = 0

    var body: some View {
        self.$childCount.update(tracked: self.masterCount) { (old, myCount) -> Int in
            if self.masterCount != old {
                return 0
            }
            return myCount
        }
     return Button("Child Count: \(self.childCount)") {
            self.childCount += 1
        }
    }
}
1 голос
/ 08 февраля 2020

следуя вашему коду, я бы сделал что-то вроде этого.

struct AlarmEditor: View {

  var handleSave : (Date) -> Void

  @State var editingDate : Date

  init(seedDate: Date, handleSave: @escaping (Date) -> Void) {
    self._editingDate = State(initialValue: seedDate)
    self.handleSave = handleSave
  }

  var body: some View {
    Form {
      DatePicker(
        selection: $editingDate,
        displayedComponents: .hourAndMinute,
        label: { Text("Date") }
      )
      Spacer()
      Button(action: {
        self.handleSave(self.editingDate)
      }) {
        Text("Save").font(.headline).bold()
      }
    }
  }//body

}//AlarmEditor

struct AlarmEditor_Previews: PreviewProvider {
  static var previews: some View {
    AlarmEditor(seedDate: Date()) { editingDate in
      print(editingDate.description)
    }
  }
}

И используйте это как-нибудь в другом месте.

AlarmEditor(seedDate: Date()) { editingDate in
  //do anything you want with editingDate
  print(editingDate.description)
}

это мой пример вывода:

2020-02-07 23:39:42 +0000
2020-02-07 22:39:42 +0000
2020-02-07 23:39:42 +0000
2020-02-07 21:39:42 +0000

что ты думаешь? 50 баллов

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...