检测用户何时停止在 SwiftUI (macOS) 中的 TextEditor 上打字
Detect when a user stopped typing on a TextEditor in SwiftUI (macOS)
我正在将 TextEditor
的文本保存到文本文件中。我首先使用 TextEditor
的第一行作为文件名创建文件,然后将后续更新保存在该文件中。该代码依赖于 .onChange
操作。这是一个挑战,因为我正在为 TextEditor
.
第一行中使用类型的每个字符创建一个文件
有没有办法检测用户何时停止输入或组件空闲,然后创建文件并保存文本?这是在 macOS Big Sur 上。
我找不到任何可以使用的操作。视图代码如下:
@EnvironmentObject private var data: DataModel
var note: NoteItem
@State var text: String
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text)
//.onTapGesture {
// tried with this....
// print("stopped typing")
//}
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.notes[index].date = Date()
data.notes[index].filename = value.components(separatedBy: NSCharacterSet.newlines).first!
saveFile(filename: data.notes[index].filename, contents: data.notes[index].text)
})
}
}
对于这种情况,Combine 有 debounce
。它适用于 Publisher
,只有在 debounceFor
指定时间内没有收到新值时才将值传递到链中。
这正是您所需要的:如果用户在一秒钟内没有输入任何内容,您应该保存文本。
请考虑以下事实:如果用户关闭视图或最小化应用程序,您可能不会保存文本状态。对于这些情况,您必须复制 onDisappear
中的保存逻辑并监听 willEnterForegroundNotification
通知。
我基于debounce
创建了一个在SwiftUI中易于使用的修改器:
import Combine
extension View {
func onDebouncedChange<V>(
of binding: Binding<V>,
debounceFor dueTime: TimeInterval,
perform action: @escaping (V) -> Void
) -> some View where V: Equatable {
modifier(ListenDebounce(binding: binding, dueTime: dueTime, action: action))
}
}
private struct ListenDebounce<Value: Equatable>: ViewModifier {
@Binding
var binding: Value
@StateObject
var debounceSubject: ObservableDebounceSubject<Value, Never>
let action: (Value) -> Void
init(binding: Binding<Value>, dueTime: TimeInterval, action: @escaping (Value) -> Void) {
_binding = binding
_debounceSubject = .init(wrappedValue: .init(dueTime: dueTime))
self.action = action
}
func body(content: Content) -> some View {
content
.onChange(of: binding) { value in
debounceSubject.send(value)
}
.onReceive(debounceSubject) { value in
action(value)
}
}
}
private final class ObservableDebounceSubject<Output: Equatable, Failure>: Subject, ObservableObject where Failure: Error {
private let passthroughSubject = PassthroughSubject<Output, Failure>()
let dueTime: TimeInterval
init(dueTime: TimeInterval) {
self.dueTime = dueTime
}
func send(_ value: Output) {
passthroughSubject.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
passthroughSubject.send(completion: completion)
}
func send(subscription: Subscription) {
passthroughSubject.send(subscription: subscription)
}
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
passthroughSubject
.removeDuplicates()
.debounce(for: .init(dueTime), scheduler: RunLoop.main)
.receive(subscriber: subscriber)
}
}
用法:
@EnvironmentObject private var data: DataModel
var note: NoteItem
@State var text: String
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text)
//.onTapGesture {
// tried with this....
// print("stopped typing")
//}
.onDebouncedChange(
of: $text,
debounceFor: 1 // TimeInterval, i.e. sec
) { value in
guard let index = data.notes.firstIndex(of: note) else {
return
}
data.notes[index].text = value
data.notes[index].date = Date()
data.notes[index].filename = value.components(separatedBy: NSCharacterSet.newlines).first!
saveFile(filename: data.notes[index].filename, contents: data.notes[index].text)
}
}
}
}
我正在将 TextEditor
的文本保存到文本文件中。我首先使用 TextEditor
的第一行作为文件名创建文件,然后将后续更新保存在该文件中。该代码依赖于 .onChange
操作。这是一个挑战,因为我正在为 TextEditor
.
有没有办法检测用户何时停止输入或组件空闲,然后创建文件并保存文本?这是在 macOS Big Sur 上。
我找不到任何可以使用的操作。视图代码如下:
@EnvironmentObject private var data: DataModel
var note: NoteItem
@State var text: String
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text)
//.onTapGesture {
// tried with this....
// print("stopped typing")
//}
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.notes[index].date = Date()
data.notes[index].filename = value.components(separatedBy: NSCharacterSet.newlines).first!
saveFile(filename: data.notes[index].filename, contents: data.notes[index].text)
})
}
}
对于这种情况,Combine 有 debounce
。它适用于 Publisher
,只有在 debounceFor
指定时间内没有收到新值时才将值传递到链中。
这正是您所需要的:如果用户在一秒钟内没有输入任何内容,您应该保存文本。
请考虑以下事实:如果用户关闭视图或最小化应用程序,您可能不会保存文本状态。对于这些情况,您必须复制 onDisappear
中的保存逻辑并监听 willEnterForegroundNotification
通知。
我基于debounce
创建了一个在SwiftUI中易于使用的修改器:
import Combine
extension View {
func onDebouncedChange<V>(
of binding: Binding<V>,
debounceFor dueTime: TimeInterval,
perform action: @escaping (V) -> Void
) -> some View where V: Equatable {
modifier(ListenDebounce(binding: binding, dueTime: dueTime, action: action))
}
}
private struct ListenDebounce<Value: Equatable>: ViewModifier {
@Binding
var binding: Value
@StateObject
var debounceSubject: ObservableDebounceSubject<Value, Never>
let action: (Value) -> Void
init(binding: Binding<Value>, dueTime: TimeInterval, action: @escaping (Value) -> Void) {
_binding = binding
_debounceSubject = .init(wrappedValue: .init(dueTime: dueTime))
self.action = action
}
func body(content: Content) -> some View {
content
.onChange(of: binding) { value in
debounceSubject.send(value)
}
.onReceive(debounceSubject) { value in
action(value)
}
}
}
private final class ObservableDebounceSubject<Output: Equatable, Failure>: Subject, ObservableObject where Failure: Error {
private let passthroughSubject = PassthroughSubject<Output, Failure>()
let dueTime: TimeInterval
init(dueTime: TimeInterval) {
self.dueTime = dueTime
}
func send(_ value: Output) {
passthroughSubject.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
passthroughSubject.send(completion: completion)
}
func send(subscription: Subscription) {
passthroughSubject.send(subscription: subscription)
}
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
passthroughSubject
.removeDuplicates()
.debounce(for: .init(dueTime), scheduler: RunLoop.main)
.receive(subscriber: subscriber)
}
}
用法:
@EnvironmentObject private var data: DataModel
var note: NoteItem
@State var text: String
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text)
//.onTapGesture {
// tried with this....
// print("stopped typing")
//}
.onDebouncedChange(
of: $text,
debounceFor: 1 // TimeInterval, i.e. sec
) { value in
guard let index = data.notes.firstIndex(of: note) else {
return
}
data.notes[index].text = value
data.notes[index].date = Date()
data.notes[index].filename = value.components(separatedBy: NSCharacterSet.newlines).first!
saveFile(filename: data.notes[index].filename, contents: data.notes[index].text)
}
}
}
}