/SaferFixedTextField.swift Secret
Created
April 17, 2022 22:51
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
import SwiftUI | |
protocol AnyStateOrBinding { | |
var anyWrappedValue: Any? { get nonmutating set } | |
} | |
extension State: AnyStateOrBinding { | |
var anyWrappedValue: Any? { | |
get { wrappedValue } | |
nonmutating set { | |
guard let newValue = newValue as? Value else { return } | |
wrappedValue = newValue | |
} | |
} | |
} | |
extension Binding: AnyStateOrBinding { | |
var anyWrappedValue: Any? { | |
get { wrappedValue } | |
nonmutating set { | |
guard let newValue = newValue as? Value else { return } | |
wrappedValue = newValue | |
} | |
} | |
} | |
extension TextField { | |
func fixed() -> some View { | |
FixedTextField(textField: self) | |
} | |
} | |
// FIXME: This forces FixedTextField to update when stateOrBinding is State<TextFieldState> | |
// maybe this can be avoided some how though... | |
extension TextField: DynamicProperty {} | |
struct FixedTextField<Label: View>: View { | |
let textField: TextField<Label> | |
@State private var lastGoodState: Any? | |
@State private var requiresCheck: Bool = false | |
@State private var didSetState = false | |
var body: some View { | |
textField | |
// NB: .onChange(of:) ordering is important for macOS | |
.onChange(of: requiresCheck) { didCheckState in | |
self.requiresCheck = false | |
guard | |
!didCheckState, | |
self.text != self.displayText, | |
let state = self.lastGoodState | |
else { return } | |
self.stateOrBinding?.anyWrappedValue = state | |
self.didSetState = true | |
} | |
.onChange(of: displayText) { newText in | |
if self.displayText == self.text { | |
self.lastGoodState = self.state | |
} | |
self.requiresCheck = !self.didSetState | |
self.didSetState = false | |
} | |
} | |
var text: String? { | |
Mirror(reflecting: textField).descendant("_text", "_value") as? String | |
} | |
var displayText: String? { | |
state.flatMap { Mirror(reflecting: $0).descendant("displayText") as? String } | |
} | |
var state: Any? { | |
stateOrBinding.flatMap { Mirror(reflecting: $0).descendant("_value") } | |
} | |
var stateOrBinding: AnyStateOrBinding? { | |
Mirror(reflecting: textField).descendant("_state", "state") as? AnyStateOrBinding | |
?? Mirror(reflecting: textField).descendant("_state", "binding") as? AnyStateOrBinding | |
} | |
} | |
struct FixedTextFieldStyle<Base: TextFieldStyle>: TextFieldStyle { | |
let base: Base | |
func _body(configuration: TextField<_Label>) -> some View { | |
configuration.fixed().textFieldStyle(base) | |
} | |
} | |
extension TextFieldStyle where Self == FixedTextFieldStyle<DefaultTextFieldStyle> { | |
static var fixed: Self { .init(base: .automatic) } | |
} | |
extension TextFieldStyle { | |
static func fixed<Base: TextFieldStyle>(_ base: Base) -> FixedTextFieldStyle<Base> | |
where Self == FixedTextFieldStyle<Base> { | |
.init(base: base) | |
} | |
} | |
struct ContentView: View { | |
@State var text = "" | |
var body: some View { | |
HStack { | |
TextField("Type here", text: Binding { text } set: { text = String($0.prefix(5)) }) | |
.fixed() // stateOrBinding is State<TextFieldState> | |
Text(text) | |
} | |
.padding() | |
// .textFieldStyle(.fixed(.roundedBorder)) // stateOrBinding == Binding<TextFieldState> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment