【问题标题】:SwiftUI: Get notified when @Binding value changesSwiftUI:@Binding 值更改时收到通知
【发布时间】:2020-02-10 07:09:39
【问题描述】:

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

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

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() }
            })
        }
    }
}

【问题讨论】:

    标签: binding swiftui


    【解决方案1】:

    使用onChange modifier 而不是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() }
                })
            }
        }
    }
    

    兼容性

    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)
        }
    }
    

    【讨论】:

    • 我试过了,它可以直接工作。然后我读了代码,意识到这个答案太聪明了!只需使用视图,因为它知道何时需要更新,然后执行分配的操作
    • 这是一个很好的介绍。我只是将它与 ScrollViewReader 结合使用,以便在它发生变化时滚动到“选定”项目,它就像一个魅力。
    • @Binding 在这里是一个错误,使用 onChange 修复错误会使情况变得更糟。 textString 应该是一个 let,因为这个 View 不会写入它。
    【解决方案2】:

    基于@Damiaan Dufaux 的answer 复制和粘贴解决方案。

    1. 就像系统.onChange API一样使用它。它更喜欢在 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
                    }
                }
            }
        }
    }
    

    【讨论】:

    • “更改为相同值时不会调用操作”谢谢,这是其他答案所缺少的。
    【解决方案3】:

    你可以像这样使用textString.wrappedValue

     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() }
                  })
              }
              .onChange(of: $textString.wrappedValue, perform: { value in
                      print(value)
              })
          }
      }
    

    【讨论】:

    • onChange is > iOS 14 和上面描述的
    【解决方案4】:

    @Binding 仅应在您的子 View 需要写入值时使用。在您的情况下,您只需要阅读它,因此将其更改为 let textString:Stringbody 将在每次更改时运行。也就是使用父 View 中的新值重新创建此 View 时。这就是 SwiftUI 的工作原理,它仅在 View 结构变量(或 let)自上次创建 View 后发生更改时才运行主体。

    【讨论】:

      【解决方案5】:

      您可以使用 onReceiveJust 包装器在 iOS 13 中使用它。

      struct TypewriterTextView: View {
          @Binding var textString:String
          @State private var typingInterval = 0.3
          @State private var typedString = ""
      
          var body: some View {
              Text(typedString)
                .onReceive(Just(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() }
                  })
              }
          }
      }
      

      【讨论】:

        【解决方案6】:

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

        public let subject = PassthroughSubject<String, Never>()
        

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

        self.subject.send()
        

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

        现在您需要在您的 SwiftUI 代码中收到以下信息:

        Text(typedString)
            .onReceive(<...>.subject)
            { (string) in
                self.typedString = string
            }
        

        &lt;...&gt; 需要替换为您的 subject 对象所在的位置。例如(作为AppDelegate 的黑客攻击):

         .onReceive((UIApplication.shared.delegate as! AppDelegate).subject)
        

        我知道当typedString@State 时,上述方法应该有效:

        @State private var typedString = ""
        

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-06-23
          • 1970-01-01
          • 1970-01-01
          • 2017-04-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-03-22
          相关资源
          最近更新 更多