Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
*.DS_Store
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
*.generated.swift

## Build generated
build/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
xcuserdata/
xcshareddata/
/.swiftpm
*.xcuserstate

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint

# Swift Package Manager
.build/
*Package.resolved
Package.resolved*

# SwiftLint
lintOutput.json

This file was deleted.

2 changes: 0 additions & 2 deletions ExampleApp/ExampleApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

return true
}
}

6 changes: 3 additions & 3 deletions ExampleApp/ExampleApp/InputFields/SecureInputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import FormView
struct SecureInputField: View {
let title: LocalizedStringKey
let text: Binding<String>
let failedRules: [TextValidationRule]
let failedRules: [ValidationRule]

@FocusState private var isFocused: Bool
@State private var isSecure = true
Expand All @@ -24,8 +24,8 @@ struct SecureInputField: View {
eyeImage
}
.background(Color.white)
if failedRules.isEmpty == false {
Text(failedRules[0].message)
if failedRules.isEmpty == false, let message = failedRules[0].message {
Text(message)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.red)
}
Expand Down
20 changes: 15 additions & 5 deletions ExampleApp/ExampleApp/InputFields/TextInputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,30 @@ import FormView

struct TextInputField: View {
let title: LocalizedStringKey
let text: Binding<String>
let failedRules: [TextValidationRule]
@Binding var text: String
let failedRules: [ValidationRule]

var body: some View {
VStack(alignment: .leading) {
TextField(title, text: text)
TextField(title, text: $text)
.background(Color.white)
if failedRules.isEmpty == false {
Text(failedRules[0].message)
if let errorMessage = failedRules.first?.message {
Text(errorMessage)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.red)
}
Spacer()
}
.frame(height: 50)
}

init(
title: LocalizedStringKey,
text: Binding<String>,
failedRules: [ValidationRule]
) {
self.title = title
self._text = text
self.failedRules = failedRules
}
}
6 changes: 3 additions & 3 deletions ExampleApp/ExampleApp/MyRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import FormView

extension TextValidationRule {
extension ValidationRule {
static var myRule: Self {
TextValidationRule(message: "Shold contain T") {
$0.contains("T")
Self.custom {
return $0.contains("T") ? nil : "Should contain T"
}
}
}
36 changes: 16 additions & 20 deletions ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,46 @@ struct ContentView: View {

var body: some View {
FormView(
validate: .never,
validate: .onFieldValueChanged,
hideError: .onValueChanged
) { proxy in
FormField(
value: $viewModel.name,
rules: [
TextValidationRule.noSpecialCharacters(message: "No spec chars"),
.notEmpty(message: "Name empty"),
.myRule
]
rules: viewModel.nameValidationRules
) { failedRules in
TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.age,
rules: [
TextValidationRule.digitsOnly(message: "Digits only"),
.maxLength(count: 2, message: "Max length 2")
]
rules: viewModel.ageValidationRules
) { failedRules in
TextInputField(title: "Age", text: $viewModel.age, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.pass,
rules: [
TextValidationRule.atLeastOneDigit(message: "One digit"),
.atLeastOneLetter(message: "One letter"),
.notEmpty(message: "Pass not empty")
]
rules: viewModel.passValidationRules
) { failedRules in
SecureInputField(title: "Password", text: $viewModel.pass, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.confirmPass,
rules: [
TextValidationRule.equalTo(value: viewModel.pass, message: "Not equal to pass"),
.notEmpty(message: "Confirm pass not empty")
]
rules: viewModel.confirmPassValidationRules
) { failedRules in
SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
if viewModel.isLoading {
ProgressView()
}
Button("Validate") {
print("Form is valid: \(proxy.validate())")
Task {
print("Form is valid: \(await proxy.validate())")
}
}
.disabled(viewModel.isLoading)
}
.padding(.horizontal, 16)
.padding(.top, 40)
Expand Down
51 changes: 51 additions & 0 deletions ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,69 @@
//

import SwiftUI
import FormView

class ContentViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: String = ""
@Published var pass: String = ""
@Published var confirmPass: String = ""
@Published var isLoading = false

var nameValidationRules: [ValidationRule] = []
var ageValidationRules: [ValidationRule] = []
var passValidationRules: [ValidationRule] = []
var confirmPassValidationRules: [ValidationRule] = []

private let coordinator: ContentCoordinator

init(coordinator: ContentCoordinator) {
self.coordinator = coordinator
print("init ContentViewModel")

setupValidationRules()
}

private func setupValidationRules() {
nameValidationRules = [
ValidationRule.notEmpty(message: "Name empty"),
ValidationRule.noSpecialCharacters(message: "No spec chars"),
ValidationRule.myRule,
ValidationRule.external { [weak self] in await self?.availabilityCheckAsync($0) }
]

ageValidationRules = [
ValidationRule.digitsOnly(message: "Digits only"),
ValidationRule.maxLength(count: 2, message: "Max length 2")
]

passValidationRules = [
ValidationRule.atLeastOneDigit(message: "One digit"),
ValidationRule.atLeastOneLetter(message: "One letter"),
ValidationRule.notEmpty(message: "Pass not empty")
]

confirmPassValidationRules = [
ValidationRule.notEmpty(message: "Confirm pass not empty"),
ValidationRule.custom { [weak self] in
return $0 == self?.pass ? nil : "Not equal to pass"
}
]
}

@MainActor
private func availabilityCheckAsync(_ value: String) async -> String? {
print(#function)

isLoading = true

try? await Task.sleep(nanoseconds: 2_000_000_000)

let isAvailable = Bool.random()

isLoading = false

return isAvailable ? nil : "Not available"
}

deinit {
Expand Down
14 changes: 0 additions & 14 deletions Package.resolved

This file was deleted.

11 changes: 5 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ let package = Package(
products: [
.library(
name: "FormView",
targets: ["FormView"])
targets: ["FormView"]
)
],
dependencies: [
.package(url: "https://github.com/nalexn/ViewInspector", branch: "master")
.package(url: "https://github.com/nalexn/ViewInspector", exact: "0.10.1")
],
targets: [
.target(
name: "FormView",
dependencies: []),
.testTarget(
name: "FormViewTests",
dependencies: ["FormView", "ViewInspector"])
dependencies: []
)
]
)
Loading