Last active
January 17, 2023 07:34
-
-
Save bsorrentino/3bd923b85ce0e8421b59c87f8f470874 to your computer and use it in GitHub Desktop.
SwiftUI: Property Wrapper for Nested ObservableObjects
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
// | |
// @ref https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ | |
// @Ref https://stackoverflow.com/a/58406402/521197 | |
// | |
@propertyWrapper | |
struct NestedObservableObject<Value : ObservableObject> { | |
static subscript<T: ObservableObject>( | |
_enclosingInstance instance: T, | |
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>, | |
storage storageKeyPath: ReferenceWritableKeyPath<T, Self> | |
) -> Value { | |
get { | |
if instance[keyPath: storageKeyPath].cancellable == nil, let publisher = instance.objectWillChange as? ObservableObjectPublisher { | |
instance[keyPath: storageKeyPath].cancellable = | |
instance[keyPath: storageKeyPath].storage.objectWillChange.sink { _ in | |
publisher.send() | |
} | |
} | |
return instance[keyPath: storageKeyPath].storage | |
} | |
set { | |
if let cancellable = instance[keyPath: storageKeyPath].cancellable { | |
cancellable.cancel() | |
} | |
if let publisher = instance.objectWillChange as? ObservableObjectPublisher { | |
instance[keyPath: storageKeyPath].cancellable = | |
newValue.objectWillChange.sink { _ in | |
publisher.send() | |
} | |
} | |
instance[keyPath: storageKeyPath].storage = newValue | |
} | |
} | |
@available(*, unavailable, | |
message: "This property wrapper can only be applied to classes" | |
) | |
var wrappedValue: Value { | |
get { fatalError() } | |
set { fatalError() } | |
} | |
private var cancellable: AnyCancellable? | |
private var storage: Value | |
init(wrappedValue: Value) { | |
storage = wrappedValue | |
} | |
} |
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 | |
import Combine | |
import FieldValidatorLibrary | |
extension FormWithValidator { | |
class ViewModel: NSObject, ObservableObject { | |
@Published var username:String = "" // observable property | |
@Published var password:String = "" // observable property | |
@NestedObservableObject var usernameValid = FieldChecker2<String>() // validation state of username field | |
@NestedObservableObject var passwordValid = FieldChecker2<String>() // validation state of username field | |
} | |
} | |
struct FormWithValidator : View { | |
@StateObject var viewModel = ViewModel() | |
func username() -> some View { | |
TextField( "give me the email", | |
text: $viewModel.username.onValidate(checker: viewModel.usernameValid, debounceInMills: 500) { v in | |
// validation closure where ‘v’ is the current value | |
if( v.isEmpty ) { | |
return "value cannot be empty" | |
} | |
return nil | |
}, onCommit: submit) | |
.autocapitalization(.none) | |
.padding( .bottom, 25 ) | |
.modifier( ValidatorMessageModifier(message: viewModel.usernameValid.errorMessage)) | |
} | |
func passwordToggle() -> some View { | |
HStack { | |
SecureField( "give me the password", | |
text: $viewModel.password.onValidate( checker: viewModel.passwordValid ) { v in | |
if( v.isEmpty ) { | |
return "password cannot be empty" | |
} | |
return nil | |
}) | |
.autocapitalization(.none) | |
.padding( .bottom, 25 ) | |
}.modifier( ValidatorMessageModifier(message: viewModel.passwordValid.errorMessage)) | |
} | |
var isValid:Bool { | |
viewModel.passwordValid.valid && viewModel.usernameValid.valid | |
} | |
func submit() { | |
if( isValid ) { | |
print( "submit:\nusername:\(self.viewModel.username)\npassword:\(self.viewModel.password)") | |
} | |
} | |
var body: some View { | |
NavigationView { | |
Form { | |
Section(header: Text("Credentials")) { | |
username() | |
passwordToggle() | |
} // end of section | |
Section { | |
HStack { | |
Spacer() | |
Button( "Submit", action: submit ) | |
// enable button only if username and password are validb | |
.disabled( !self.isValid ) | |
Spacer() | |
} | |
} // end of section | |
} // end of form | |
.navigationBarTitle( Text( "Validation 1.5 Sample" ), displayMode: .inline ) | |
} // NavigationView | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment