Skip to content

Instantly share code, notes, and snippets.

@cutiko
Created April 18, 2024 16:00
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 cutiko/d97e39e509ed0d2eed62994d4728b140 to your computer and use it in GitHub Desktop.
Save cutiko/d97e39e509ed0d2eed62994d4728b140 to your computer and use it in GitHub Desktop.
Hoisted state for forms in iOS (with UI Model)
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel = FormViewModel()
var body: some View {
VStack {
SimpleInput($viewModel.screenState.firstInput) {
print("CUTIKO_TAG FIRST focus lost")
viewModel.validateFirstInput()
}
Text(viewModel.screenState.firstInputError)
SimpleInput($viewModel.screenState.secondInput) {
print("CUTIKO_TAG SECOND focus lost")
viewModel.validateSecondInput()
}
Text(viewModel.screenState.secondInputError)
SimpleInput($viewModel.screenState.thirdInput) {
print("CUTIKO_TAG THIRD focus lost")
}
Text(verbatim: "isEnabled: \(viewModel.screenState.isValid)")
Button(
action: {
print("CUTIKO_TAG UI MODEL IS: \(viewModel.screenState)")
},
label: {
Text("Validate UI MODEL in logs")
}
)
}
.padding()
}
}
private struct SimpleInput: View {
var text: Binding<String>
@FocusState private var isFocused: Bool
private let onFocusLost: () -> Void
@State private var previousFocus = false
init(_ text: Binding<String>, onFocusLost: @escaping () -> Void) {
self.text = text
self.onFocusLost = onFocusLost
}
var body: some View {
TextField(
"INPUT:",
text: self.text
)
.focused($isFocused)
.onChange(of: isFocused) { newFocus in
if (previousFocus && !newFocus) {
self.onFocusLost()
}
previousFocus = newFocus
}
}
}
import Foundation
struct FormState {
private static let noError = "NO ERROR"
private var firstInputLostFocus = false
var firstInput: String = "" {
didSet {
if firstInputLostFocus { validateFirstInput() }
}
}
private var secondInputLostFocus = false
var firstInputError = FormState.noError
var secondInput = "" {
didSet {
if secondInputLostFocus { validateSecondInput() }
}
}
var secondInputError = FormState.noError
var thirdInput = ""
var isValid: Bool {
return isFirstInputValid() && isSecondInputValid() && !thirdInput.isEmpty
}
private func isFirstInputValid() -> Bool {
return !firstInput.isEmpty
}
private func isSecondInputValid() -> Bool {
return secondInput.count >= 3
}
mutating func validateFirstInput() {
firstInputLostFocus = true
if isFirstInputValid() {
firstInputError = FormState.noError
} else {
firstInputError = "PLEASE WRITE THE FIRST INPUT"
}
}
mutating func validateSecondInput() {
secondInputLostFocus = true
if isSecondInputValid() {
secondInputError = FormState.noError
} else {
secondInputError = "Second input is VERY important, 3 char min"
}
}
}
extension FormState {
static let initialValue = FormState()
}
import Foundation
class FormViewModel: ObservableObject {
@Published var screenState = FormState.initialValue
func validateFirstInput() {
screenState.validateFirstInput()
}
func validateSecondInput() {
screenState.validateSecondInput()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment