Last active
October 11, 2022 19:34
-
-
Save allenhumphreys/b6da04f6123bb7b794794d12dc5f7506 to your computer and use it in GitHub Desktop.
SwiftUI Focus 2-way Binding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol FocusTracking<FocusField>: AnyObject { | |
associatedtype FocusField: Hashable | |
var focusedField: FocusField? { get set } | |
} | |
struct FocusTrackingView<Content, Target: FocusTracking>: View where Content: View, Target.FocusField: Hashable { | |
@FocusState.Binding var focusedField: Target.FocusField? | |
let content: Content | |
let boundTo: Target | |
let defaultValue: Target.FocusField? | |
let defaultValueDelay: Double? | |
@inlinable public init( | |
focusedFieldBinding: FocusState<Target.FocusField?>.Binding, | |
boundTo: Target, | |
defaultValue: Target.FocusField? = nil, | |
defaultValueDelay: Double? = nil, | |
@ViewBuilder content: () -> Content | |
) { | |
_focusedField = focusedFieldBinding | |
self.boundTo = boundTo | |
self.defaultValue = defaultValue | |
self.defaultValueDelay = defaultValueDelay | |
self.content = content() | |
} | |
var body: some View { | |
content | |
.onAppear { | |
guard let defaultValue = defaultValue else { return } | |
let setDefault = { | |
focusedField = defaultValue | |
} | |
// Delay is needed otherwise the focus won't actually be applied | |
if let delay = defaultValueDelay { | |
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: setDefault) | |
} else { | |
setDefault() | |
} | |
} | |
.onChange(of: boundTo.focusedField) { newValue in | |
focusedField = newValue | |
} | |
.onChange(of: focusedField) { newValue in | |
boundTo.focusedField = newValue | |
} | |
} | |
} | |
extension View { | |
func bindFocus<T: FocusTracking>( | |
from: FocusState<T.FocusField?>.Binding, | |
to: T, defaultFocus: T.FocusField? = nil, // swiftlint:disable:this identifier_name | |
defaultValueDelay: Double? = nil | |
) -> some View { | |
FocusTrackingView(focusedFieldBinding: from, boundTo: to, defaultValue: defaultFocus, defaultValueDelay: defaultValueDelay) { | |
self | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment