我写了一个视图在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() }
})
}
}
}
答案 0 :(得分:11)
使用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上引入的,仅在
如果要在较旧的系统上使用此功能,可以使用以下垫片。基本上,它是使用较旧的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)
}
}
答案 1 :(得分:1)
根据@Damiaan Dufaux的答案复制并粘贴解决方案。
onChange
API一样使用它。它更喜欢在iOS 14上使用系统提供的onChange
API,并在较低版本上使用备份计划。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
}
}
}
}
}
答案 2 :(得分:1)
@Binding
应该仅在您的子视图需要写入值时使用。在您的情况下,您只需要阅读它,因此将其更改为 let textString:String
并且每次更改时 body
都会运行。这是在使用父 View
中的新值重新创建此 View
时。这就是 SwiftUI 的工作方式,它仅在 View
结构变量(或 let)自上次创建 View
以来发生更改时才运行正文。
答案 3 :(得分:0)
您可以为此使用所谓的发布者:
public let subject = PassthroughSubject<String, Never>()
然后,在计时器块内调用:
self.subject.send()
通常,您希望以上代码在SwiftUI UI声明之外。
现在,在您的SwiftUI代码中,您需要接收以下信息:
Text(typedString)
.onReceive(<...>.subject)
{ (string) in
self.typedString = string
}
<...>
需要替换为subject
对象所在的位置。例如(作为对AppDelegate
的黑客攻击):
.onReceive((UIApplication.shared.delegate as! AppDelegate).subject)
我知道以上情况应该在typedString
是@State
时起作用:
@State private var typedString = ""
但是我想它也应该和@Binding
一起工作;只是还没有尝试过。
答案 4 :(得分:0)
您可以像这样使用 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)
})
}
}
答案 5 :(得分:0)
您可以将 onReceive
与 Just
包装器结合使用,以便在 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() }
})
}
}
}