SwiftUI:在@Binding值更改时收到通知
我写了一个视图来在SwiftUI中创建打字机效果——当我第一次传入绑定变量时,它工作正常,例如:TypewriterTextView($textString) 但是,textString值的任何后续更改都将不起作用(因为绑定值不会直接放置在正文中)。我对如何在视图中更改@Binding变量时手动通知感兴趣SwiftUI:在@Binding值更改时收到通知,binding,swiftui,Binding,Swiftui,我写了一个视图来在SwiftUI中创建打字机效果——当我第一次传入绑定变量时,它工作正常,例如:TypewriterTextView($textString) 但是,textString值的任何后续更改都将不起作用(因为绑定值不会直接放置在正文中)。我对如何在视图中更改@Binding变量时手动通知感兴趣 struct TypewriterTextView: View { @Binding var textString:String @State private var typ
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() }
})
}
}
}
struct TypewriterTextView:View{
@绑定变量textString:String
@国家私有变量typingInterval=0.3
@国家私有变量typedString=“”
var body:一些观点{
Text(typedString).onAppear(){
scheduledTimer(withTimeInterval:self.typingInterval,repeats:true,block:{Timer in
如果self.typedString.length
您可以使用所谓的发布者来实现以下目的:
public let subject = PassthroughSubject<String, Never>()
通常,您希望上述代码位于SwiftUI声明之外
现在,在您的SwiftUI代码中,您需要接收以下信息:
Text(typedString)
.onReceive(<...>.subject)
{ (string) in
self.typedString = string
}
我知道当类型字符串
是@状态
时,上述操作应该有效:
.onReceive((UIApplication.shared.delegate as! AppDelegate).subject)
@State private var typedString = ""
但我想它也应该与@绑定一起工作
;只是还没有尝试过。使用而不是onAppear()
来查看文本字符串的绑定
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() }
})
}
}
}
根据@Damiaan Dufaux的答案复制并粘贴解决方案
使用它就像systemonChange
api一样。它更喜欢在iOS 14上使用系统提供的onChange
api,在较低版本上使用备份计划
当更改为相同的值时,不会调用操作。(如果使用@Damiaan Dufaux的答案,您可能会发现,如果数据更改为相同的值,则会调用操作,因为每次都会重新创建model
)
struct ChangeObserver:视图{
让内容:内容
让价值:价值
让动作:(值)->Void
init(值:值,操作:@escaping(值)->Void,内容:@escaping()->content){
自我价值=价值
行动
self.content=content()
_oldValue=状态(初始值:值)
}
@国有私有var oldValue:值
var body:一些观点{
如果oldValue!=值{
DispatchQueue.main.async{
oldValue=值
自我行动(自我价值)
}
}
返回内容
}
}
扩展视图{
func onDataChange(值为:value的,执行操作:@escaping(u-newValue:value)->Void)->一些视图{
团体{
如果可用(iOS 14.0、macOS 11.0、tvOS 14.0、watchOS 7.0、*){
onChange(of:value,perform:action)
}否则{
ChangeObserver(值:值,操作:操作){
自己
}
}
}
}
}
@Binding
只应在子视图需要写入值时使用。在您的情况下,您只需阅读它,就可以将其更改为let textString:String
,body
将在每次更改时运行。这是在父视图中用新值重新创建此视图时。这就是SwiftUI的工作原理,它只在自上次创建视图以来结构变量(或let)发生更改的情况下运行body。我尝试了它,它直接工作。然后我读了代码,意识到这个答案太聪明了!只需使用一个视图,因为它知道何时需要更新并执行分配的操作。这是一个非常好的介绍。我只是将它与ScrollViewReader结合使用,以便在“选定”项发生更改时滚动到该项,它就像一个符咒一样工作。@在这里绑定是一个错误,使用onChange修复错误会使它变得更糟textString
应该是let,因为此视图不向其写入。
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() }
})
}
}
}
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)
}
}
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
}
}
}
}
}