Created
November 7, 2019 06:07
-
-
Save 1amageek/a30daeffe17c0e8f38617f8e0f8d85ba to your computer and use it in GitHub Desktop.
SwiftUI Validation
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
struct Address { | |
var firstName: String = "" | |
var lastName: String = "" | |
var postalCode: String = "" | |
var state: String = "" | |
var city: String = "" | |
var town: String = "" | |
var line1: String = "" | |
} | |
enum ValidationResult: Hashable { | |
case valid(_ text: String) | |
case invalid(_ message: String) | |
} | |
struct Row: View { | |
struct StateView: View { | |
var state: ValidationResult? | |
var body: some View { | |
self.state.flatMap { state -> AnyView in | |
if case .invalid(_) = state { | |
return AnyView(Image(systemName: "xmark.circle") | |
.foregroundColor(Color.red)) | |
} else { | |
return AnyView(Image(systemName: "checkmark.circle") | |
.foregroundColor(Color.green)) | |
} | |
} | |
} | |
} | |
@State var state: ValidationResult? | |
var label: String | |
var placeholder: String | |
var keyPath: WritableKeyPath<Address, String> | |
var validator: ObservedObject<AnyValidator<Address, ValidationResult>> | |
var transform: (String) -> ValidationResult? | |
init(label: String, placeholder: String, keyPath: WritableKeyPath<Address, String>, validator: ObservedObject<AnyValidator<Address, ValidationResult>>, transform: @escaping (String) -> ValidationResult?) { | |
self.label = label | |
self.placeholder = placeholder | |
self.keyPath = keyPath | |
self.validator = validator | |
self.transform = transform | |
} | |
var body: some View { | |
HStack { | |
VStack(alignment: .leading, spacing: 4) { | |
HStack { | |
Text(label) | |
state.flatMap { state -> Text? in | |
if case .invalid(let message) = state { | |
return Text(message).foregroundColor(Color.red) | |
} | |
return nil | |
} | |
} | |
TextField(placeholder, text: self.validator.projectedValue[keyPath]) | |
} | |
StateView(state: state) | |
}.onAppear { | |
self.validator.wrappedValue.validate(state: self.$state, keyPath: self.keyPath, transform: self.transform) | |
} | |
} | |
} | |
struct FormView: View { | |
@ObservedObject(initialValue: AnyValidator<Address, ValidationResult>(Address())) var validator: AnyValidator | |
var body: some View { | |
Form { | |
Validator(validator: self._validator, keyPath: \.line1, content: { (result, text) -> AnyView in | |
AnyView(HStack { | |
Row.StateView(state: result) | |
TextField("placeholder", text: text) | |
}) | |
}) { text -> ValidationResult? in | |
if text.isEmpty { | |
return nil | |
} | |
if 16 < text.count { | |
return .invalid("ちょうしのんなよ。") | |
} | |
if 0 < text.count && text.count < 6 { | |
return .valid(text) | |
} | |
return .invalid("名字ながくね?") | |
} | |
Row(label: "姓", placeholder: "例)山田", keyPath: \.lastName, validator: self._validator) { text -> ValidationResult? in | |
if text.isEmpty { | |
return nil | |
} | |
if 16 < text.count { | |
return .invalid("ちょうしのんなよ。") | |
} | |
if 0 < text.count && text.count < 6 { | |
return .valid(text) | |
} | |
return .invalid("名字ながくね?") | |
} | |
Row(label: "名", placeholder: "例)太郎", keyPath: \.firstName, validator: self._validator) { text -> ValidationResult? in | |
if text.isEmpty { | |
return nil | |
} | |
if 16 < text.count { | |
return .invalid("は?🤬") | |
} | |
if 0 < text.count && text.count < 6 { | |
return .valid(text) | |
} | |
return .invalid("名前ながくね?") | |
} | |
}.onAppear { | |
} | |
} | |
} | |
struct FormView_Previews: PreviewProvider { | |
static var previews: some View { | |
FormView() | |
} | |
} |
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 | |
protocol Validatable: class { | |
associatedtype Subject | |
associatedtype Result | |
var subject: Subject { get set } | |
var cancellables: [AnyCancellable] { get } | |
func validate<T>(state: Binding<Result?>, keyPath: KeyPath<Subject, T>, transform: @escaping (T) -> Result?) | |
} | |
extension Validatable { | |
subscript<T>(keyPath: WritableKeyPath<Subject, T>) -> T { | |
get { | |
return subject[keyPath: keyPath] | |
} | |
set { | |
self.subject[keyPath: keyPath] = newValue | |
} | |
} | |
} | |
class AnyValidator<Subject, Result>: ObservableObject, Validatable { | |
@Published var subject: Subject | |
init(_ initialValue: Subject) { | |
self.subject = initialValue | |
} | |
var cancellables: [AnyCancellable] = [] | |
func validate<T>(state: Binding<Result?>, keyPath: KeyPath<Subject, T>, transform: @escaping (T) -> Result?) { | |
$subject | |
.map(keyPath) | |
.map(transform) | |
.receive(on: RunLoop.main) | |
.sink { state.wrappedValue = $0 } | |
.store(in: &cancellables) | |
} | |
} | |
struct Validator<Subject, Result, Content>: View where Content: View { | |
@State var result: Result? = nil | |
var keyPath: WritableKeyPath<Subject, String> | |
var validator: ObservedObject<AnyValidator<Subject, Result>> | |
var content: (Result?, Binding<String>) -> Content | |
var transform: (String) -> Result? | |
init(validator: ObservedObject<AnyValidator<Subject, Result>>, keyPath: WritableKeyPath<Subject, String>, @ViewBuilder content: @escaping (Result?, Binding<String>) -> Content, transform: @escaping (String) -> Result?) { | |
self.validator = validator | |
self.keyPath = keyPath | |
self.content = content | |
self.transform = transform | |
} | |
var body: some View { | |
content(self.result, self.validator.projectedValue[keyPath]) | |
.onAppear { | |
self.validator.wrappedValue.validate(state: self.$result, keyPath: self.keyPath, transform: self.transform) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment