Skip to content

Instantly share code, notes, and snippets.

@1amageek
Created November 7, 2019 06:07
Show Gist options
  • Save 1amageek/a30daeffe17c0e8f38617f8e0f8d85ba to your computer and use it in GitHub Desktop.
Save 1amageek/a30daeffe17c0e8f38617f8e0f8d85ba to your computer and use it in GitHub Desktop.
SwiftUI Validation
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()
}
}
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