SwiftUI:在@Binding 值更改时收到通知

ohs*_*apy 12 binding swiftui

我写了一个视图来在 SwiftUI 中创建打字机效果 - 当我传入绑定变量时,它第一次工作正常,例如:TypewriterTextView($textString)

但是,任何后续 textString 值更改时它将不起作用(因为绑​​定值未直接放置在正文中)。我对如何在视图中更改 @Binding 变量时手动通知的任何想法感兴趣。

struct TypewriterTextView: View {

    @Binding var textString:String
    @State private var typingInterval = 0.3
    @State private var typedString = ""

    var body: some View {
        Text(typedString).onAppear() {
            Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in

                if self.typedString.length < self.textString.length {
                    self.typedString = self.typedString + self.textString[self.typedString.length]
                }
                else { timer.invalidate() }
            })
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Dam*_*aux 18

使用onChange 修饰符而不是onAppear()观察textString绑定。

struct TypewriterTextView: View {
    @Binding var textString:String
    @State private var typingInterval = 0.3
    @State private var typedString = ""

    var body: some View {
        Text(typedString).onChange(of: textString) {
            typedString = ""
            Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in

                if self.typedString.length < self.textString.length {
                    self.typedString = self.typedString + self.textString[self.typedString.length]
                }
                else { timer.invalidate() }
            })
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

兼容性

onChange修饰符是在 WWDC 2020 上推出的,仅在

  • macOS 11+
  • iOS 14+
  • tvOS 14+
  • watchOS 7+

如果您想在旧系统上使用此功能,您可以使用以下垫片。它基本上是使用较旧的 SwiftUI 重新实现的 onChange 方法:

import Combine
import SwiftUI

/// See `View.onChange(of: value, perform: action)` for more information
struct ChangeObserver<Base: View, Value: Equatable>: View {
    let base: Base
    let value: Value
    let action: (Value)->Void

    let model = Model()

    var body: some View {
        if model.update(value: value) {
            DispatchQueue.main.async { self.action(self.value) }
        }
        return base
    }

    class Model {
        private var savedValue: Value?
        func update(value: Value) -> Bool {
            guard value != savedValue else { return false }
            savedValue = value
            return true
        }
    }
}

extension View {
    /// Adds a modifier for this view that fires an action when a specific value changes.
    ///
    /// You can use `onChange` to trigger a side effect as the result of a value changing, such as an Environment key or a Binding.
    ///
    /// `onChange` is called on the main thread. Avoid performing long-running tasks on the main thread. If you need to perform a long-running task in response to value changing, you should dispatch to a background queue.
    ///
    /// The new value is passed into the closure. The previous value may be captured by the closure to compare it to the new value. For example, in the following code example, PlayerView passes both the old and new values to the model.
    ///
    /// ```
    /// struct PlayerView : View {
    ///   var episode: Episode
    ///   @State private var playState: PlayState
    ///
    ///   var body: some View {
    ///     VStack {
    ///       Text(episode.title)
    ///       Text(episode.showTitle)
    ///       PlayButton(playState: $playState)
    ///     }
    ///   }
    ///   .onChange(of: playState) { [playState] newState in
    ///     model.playStateDidChange(from: playState, to: newState)
    ///   }
    /// }
    /// ```
    ///
    /// - Parameters:
    ///   - value: The value to check against when determining whether to run the closure.
    ///   - action: A closure to run when the value changes.
    ///   - newValue: The new value that failed the comparison check.
    /// - Returns: A modified version of this view
    func onChange<Value: Equatable>(of value: Value, perform action: @escaping (_ newValue: Value)->Void) -> ChangeObserver<Self, Value> {
        ChangeObserver(base: self, value: value, action: action)
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 8

根据@Damiaan Dufaux 的答案复制并粘贴解决方案。

  1. 就像使用系统.onChangeAPI 一样使用它。它更喜欢使用iOS 14上提供的系统.onChange,并使用较低版本的备份计划。
  2. action更改为相同值时不会被调用。(如果您使用 @Damiaan Dufaux 的答案,您可能会发现即使数据更改为相同的值也会调用该操作,因为model每次都会重新创建。)
struct ChangeObserver<Content: View, Value: Equatable>: View {
    let content: Content
    let value: Value
    let action: (Value) -> Void

    init(value: Value, action: @escaping (Value) -> Void, content: @escaping () -> Content) {
        self.value = value
        self.action = action
        self.content = content()
        _oldValue = State(initialValue: value)
    }

    @State private var oldValue: Value

    var body: some View {
        if oldValue != value {
            DispatchQueue.main.async {
                oldValue = value
                self.action(self.value)
            }
        }
        return content
    }
}

extension View {
    func onDataChange<Value: Equatable>(of value: Value, perform action: @escaping (_ newValue: Value) -> Void) -> some View {
        Group {
            if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
                self.onChange(of: value, perform: action)
            } else {
                ChangeObserver(value: value, action: action) {
                    self
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


mea*_*ers 1

您可以为此使用所谓的发布者:

public let subject = PassthroughSubject<String, Never>()
Run Code Online (Sandbox Code Playgroud)

然后,在计时器块内调用:

self.subject.send()
Run Code Online (Sandbox Code Playgroud)

通常,您希望上述代码位于 SwiftUI UI 声明之外。

现在,在您的 SwiftUI 代码中,您需要接收以下内容:

Text(typedString)
    .onReceive(<...>.subject)
    { (string) in
        self.typedString = string
    }
Run Code Online (Sandbox Code Playgroud)

<...>需要替换为您的subject对象所在的位置。例如(作为对 的 hack AppDelegate):

 .onReceive((UIApplication.shared.delegate as! AppDelegate).subject)
Run Code Online (Sandbox Code Playgroud)

我知道上面的方法应该在typedString以下情况下起作用@State

@State private var typedString = ""
Run Code Online (Sandbox Code Playgroud)

但我想它也应该适用于@Binding; 只是还没有尝试过。