Как переключить анимацию в View снаружи с подготовкой в ​​SwiftUI? - PullRequest
2 голосов
/ 22 февраля 2020

Я создаю UI-компонент с SwiftUI, который должен иметь триггер извне, чтобы включить анимацию и некоторые внутренние приготовления к ней. В следующих примерах это функция prepareArray ().

Мой первый подход состоял в том, чтобы использовать привязки, но я обнаружил, что нет способа прослушивать изменения @Binding var для запуска чего-либо:

struct ParentView: View {
    @State private var animated: Bool = false

    var body: some View {
        VStack {
            TestView(animated: $animated)
            Spacer()
            Button(action: {
                self.animated.toggle()
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    @Binding var animated: Bool {
        didSet {
           prepareArray()
        }
    }

    var body: some View {
        Text("\(array.count): \(animated ? "Y" : "N")").background(animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
    }

    private func prepareArray() {
        array = [1]
    }
}

Почему тогда он позволяет прослушивателю didSet для @Binding var, если он не работает ?! Затем я переключился на простой сигнал объединения, так как он может быть пойман при закрытии onReceive. Но @State on signal не делал недействительным представление при передаче значения:

struct ParentView: View {
    @State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)

    var body: some View {
        VStack {
            TestView(animated: animatedSignal)
            Spacer()
            Button(action: {
                self.animatedSignal.send(!self.animatedSignal.value)
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    @State var animated: CurrentValueSubject<Bool, Never>

    var body: some View {
        Text("\(array.count): \(animated.value ? "Y" : "N")").background(animated.value ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
            if animated {
                self.prepareArray()
            }
        }
    }

    private func prepareArray() {
        array = [1]
    }
}

Таким образом, мой последний подход заключался в том, чтобы вызвать внутреннее состояние var при значении сигнала:

struct ParentView: View {
    @State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)

    var body: some View {
        VStack {
            TestView(animated: animatedSignal)
            Spacer()
            Button(action: {
                self.animatedSignal.send(!self.animatedSignal.value)
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    let animated: CurrentValueSubject<Bool, Never>
    @State private var animatedInnerState: Bool = false {
        didSet {
            if animatedInnerState {
                self.prepareArray()
            }
        }
    }

    var body: some View {
        Text("\(array.count): \(animatedInnerState ? "Y" : "N")").background(animatedInnerState ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
            self.animatedInnerState = animated
        }
    }

    private func prepareArray() {
        array = [1]
    }
}

Что работает нормально, но Я не могу поверить, что такая простая задача требует такой сложной конструкции! Я знаю, что SwiftUI декларативный, но, может быть, мне не хватает более простого подхода к этой задаче? На самом деле в реальном коде этот анимированный триггер должен быть передан еще на один уровень глубже (

1 Ответ

0 голосов
/ 22 февраля 2020

Этого можно добиться разными способами, в том числе и теми, которые вы пробовали. Какой из них выбрать, может зависеть от реальных потребностей проекта. (Все проверено и работает Xcode 11.3).

Вариант 1: изменил вашу первую попытку с @Binding. Изменено только TestView.

struct TestView: View {
    @State private var array = [Int]()

    @Binding var animated: Bool
    private var myAnimated: Binding<Bool> { // internal proxy binding
        Binding<Bool>(
            get: { // called whenever external binding changed
                self.prepareArray(for: self.animated) 
                return self.animated
            },
            set: { _ in } // here not used, so just stub
        )
    }

    var body: some View {
        Text("\(array.count): \(myAnimated.wrappedValue ? "Y" : "N")")
            .background(myAnimated.wrappedValue ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
    }

    private func prepareArray(for animating: Bool) {
        DispatchQueue.main.async { // << avoid "Modifying state during update..."
            self.array = animating ? [1] : [Int]() // just example
        }
    }
}

Variant2 (мой вариант): в зависимости от модели представления и публикации, но требует изменений как ParentView, так и TestView, однако в целом проще & clear.

class ParentViewModel: ObservableObject {
    @Published var animated: Bool = false
}

struct ParentView: View {
    @ObservedObject var vm = ParentViewModel()

    var body: some View {
        VStack {
            TestView()
               .environmentObject(vm) // alternate might be via argument
            Spacer()
            Button(action: {
                self.vm.animated.toggle()
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @EnvironmentObject var parentModel: ParentViewModel
    @State private var array = [Int]()

    var body: some View {
        Text("\(array.count): \(parentModel.animated ? "Y" : "N")")
            .background(parentModel.animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
            .onReceive(parentModel.$animated) {
                self.prepareArray(for: $0)
            }
    }

    private func prepareArray(for animating: Bool) {
        self.array = animating ? [1] : [Int]() // just example
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...