perf(keyboard): track KeyboardState per property with @Observable
- KeyboardState moves from ObservableObject/@Published to @Observable, so a view re-renders only for the properties it reads. Previously unpublished state and injected action hooks are @ObservationIgnored. - KeyboardViewController replaces the $phase/$surface Combine sinks with Observations loops tied to the same cancellables. - AIKeyboardView reads the 200 ms mic level and live ASR partials in small child views (AssistantWaveform, LiveTranscriptReader), so those ticks no longer rebuild the whole assistant surface. - Tests observe changes with withObservationTracking instead of Combine publishers.
This commit is contained in:
@@ -778,28 +778,38 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
}
|
||||
|
||||
private func installSurfaceObservers() {
|
||||
state.$phase
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if self.state.locksTypingSurface, self.state.surface == .typing {
|
||||
self.applySurface(.voice)
|
||||
}
|
||||
let state = state
|
||||
// `Phase` is not Sendable; reading it only registers the dependency.
|
||||
observe(Observations { _ = state.phase }) { [weak self] in
|
||||
guard let self else { return }
|
||||
if self.state.locksTypingSurface, self.state.surface == .typing {
|
||||
self.applySurface(.voice)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
state.$surface
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] surface in
|
||||
guard let self else { return }
|
||||
if surface == .typing {
|
||||
self.activateTypingPresentationIfNeeded()
|
||||
} else {
|
||||
self.deactivateTypingSurface()
|
||||
}
|
||||
self.refreshKeyboardHeight()
|
||||
observe(Observations { state.surface }) { [weak self] surface in
|
||||
guard let self else { return }
|
||||
if surface == .typing {
|
||||
self.activateTypingPresentationIfNeeded()
|
||||
} else {
|
||||
self.deactivateTypingSurface()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
self.refreshKeyboardHeight()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `handler` for the current value and every later change. The loop
|
||||
/// is cancelled together with `cancellables` when the controller goes away.
|
||||
private func observe<Element: Sendable>(
|
||||
_ values: Observations<Element, Never>,
|
||||
handler: @escaping @MainActor (Element) -> Void
|
||||
) {
|
||||
let task = Task { @MainActor in
|
||||
for await value in values {
|
||||
handler(value)
|
||||
}
|
||||
}
|
||||
cancellables.insert(AnyCancellable { task.cancel() })
|
||||
}
|
||||
|
||||
/// This App Group property is supplied by the parallel settings module.
|
||||
|
||||
Reference in New Issue
Block a user