Skip to content

Instantly share code, notes, and snippets.

@bsorrentino
Last active January 17, 2023 07:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bsorrentino/3bd923b85ce0e8421b59c87f8f470874 to your computer and use it in GitHub Desktop.
Save bsorrentino/3bd923b85ce0e8421b59c87f8f470874 to your computer and use it in GitHub Desktop.
SwiftUI: Property Wrapper for Nested ObservableObjects
//
// @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
}
}
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