From 9e5588e067917663ebc3285f43ff92fd5dfea588 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Mon, 25 Nov 2024 16:44:44 +0300 Subject: [PATCH 01/13] Test remove validator --- ExampleApp/Example-Info.plist | 24 ++-- ExampleApp/Example.xcodeproj/project.pbxproj | 8 ++ ExampleApp/ExampleApp/AppDelegate.swift | 20 ++++ ExampleApp/ExampleApp/ContentView.swift | 6 +- ExampleApp/ExampleApp/ContentViewModel.swift | 11 ++ ExampleApp/ExampleApp/ExampleApp.swift | 111 ++++++++++++++++++- ExampleApp/ExampleApp/SceneDelegate.swift | 30 +++++ Sources/FormView/FormView.swift | 39 ++++--- 8 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 ExampleApp/ExampleApp/AppDelegate.swift create mode 100644 ExampleApp/ExampleApp/SceneDelegate.swift diff --git a/ExampleApp/Example-Info.plist b/ExampleApp/Example-Info.plist index 12d29c4..35b1fed 100644 --- a/ExampleApp/Example-Info.plist +++ b/ExampleApp/Example-Info.plist @@ -2,12 +2,22 @@ - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + diff --git a/ExampleApp/Example.xcodeproj/project.pbxproj b/ExampleApp/Example.xcodeproj/project.pbxproj index b2c99c5..6ca9530 100644 --- a/ExampleApp/Example.xcodeproj/project.pbxproj +++ b/ExampleApp/Example.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 426CF63C29850D1B00012FBE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 426CF63B29850D1B00012FBE /* Assets.xcassets */; }; 426CF63F29850D1B00012FBE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 426CF63E29850D1B00012FBE /* Preview Assets.xcassets */; }; 426CF64929850DD400012FBE /* FormView in Frameworks */ = {isa = PBXBuildFile; productRef = 426CF64829850DD400012FBE /* FormView */; }; + 739E30032CF4A1AF009B795F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739E30022CF4A1AB009B795F /* AppDelegate.swift */; }; + 739E30052CF4A1CF009B795F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739E30042CF4A1CE009B795F /* SceneDelegate.swift */; }; E1DB5A1F2A73BFCF0024C47A /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */; }; E1E8FCDB2A52B8CD0099A852 /* SecureInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E8FCDA2A52B8CD0099A852 /* SecureInputField.swift */; }; /* End PBXBuildFile section */ @@ -28,6 +30,8 @@ 426CF63E29850D1B00012FBE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 426CF64629850D9F00012FBE /* FormView */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FormView; path = ..; sourceTree = ""; }; 426CF64C29903DBC00012FBE /* Example-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Example-Info.plist"; sourceTree = ""; }; + 739E30022CF4A1AB009B795F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 739E30042CF4A1CE009B795F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; E1E8FCDA2A52B8CD0099A852 /* SecureInputField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureInputField.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -66,6 +70,8 @@ 426CF63629850D1A00012FBE /* ExampleApp */ = { isa = PBXGroup; children = ( + 739E30022CF4A1AB009B795F /* AppDelegate.swift */, + 739E30042CF4A1CE009B795F /* SceneDelegate.swift */, 426CF63729850D1A00012FBE /* ExampleApp.swift */, 426CF63929850D1A00012FBE /* ContentView.swift */, E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */, @@ -209,8 +215,10 @@ E1E8FCDB2A52B8CD0099A852 /* SecureInputField.swift in Sources */, E1DB5A1F2A73BFCF0024C47A /* ContentViewModel.swift in Sources */, 426A30E529E81D5A00C5FB02 /* MyRule.swift in Sources */, + 739E30032CF4A1AF009B795F /* AppDelegate.swift in Sources */, 426CF63A29850D1A00012FBE /* ContentView.swift in Sources */, 426CF63829850D1A00012FBE /* ExampleApp.swift in Sources */, + 739E30052CF4A1CF009B795F /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ExampleApp/ExampleApp/AppDelegate.swift b/ExampleApp/ExampleApp/AppDelegate.swift new file mode 100644 index 0000000..115016c --- /dev/null +++ b/ExampleApp/ExampleApp/AppDelegate.swift @@ -0,0 +1,20 @@ +// +// AppDelegate.swift +// Example +// +// Created by Victor Kostin on 25.11.2024. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + return true + } +} + diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift index 885254c..029f792 100644 --- a/ExampleApp/ExampleApp/ContentView.swift +++ b/ExampleApp/ExampleApp/ContentView.swift @@ -15,7 +15,7 @@ struct ContentView: View { FormView( validate: .never, hideError: .onValueChanged - ) { proxy in + ) { validate in FormField( value: $viewModel.name, rules: [ @@ -55,7 +55,7 @@ struct ContentView: View { SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules) } Button("Validate") { - print("Form is valid: \(proxy.validate())") + print("Form is valid: \(proxy(true))") } } .padding(.horizontal, 16) @@ -70,6 +70,6 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(viewModel: ContentViewModel()) + ContentView(viewModel: ContentViewModel(coordinator: ContentCoordinator())) } } diff --git a/ExampleApp/ExampleApp/ContentViewModel.swift b/ExampleApp/ExampleApp/ContentViewModel.swift index 260ede2..6816ecd 100644 --- a/ExampleApp/ExampleApp/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/ContentViewModel.swift @@ -12,4 +12,15 @@ class ContentViewModel: ObservableObject { @Published var age: String = "" @Published var pass: String = "" @Published var confirmPass: String = "" + + let coordinator: ContentCoordinator + + init(coordinator: ContentCoordinator) { + self.coordinator = coordinator + print("init ContentViewModel") + } + + deinit { + print("deinit ContentViewModel") + } } diff --git a/ExampleApp/ExampleApp/ExampleApp.swift b/ExampleApp/ExampleApp/ExampleApp.swift index 18ea7c9..a6fbc8a 100644 --- a/ExampleApp/ExampleApp/ExampleApp.swift +++ b/ExampleApp/ExampleApp/ExampleApp.swift @@ -6,12 +6,113 @@ // import SwiftUI +import UIKit -@main -struct ExampleApp: App { - var body: some Scene { - WindowGroup { - ContentView(viewModel: ContentViewModel()) +class HostingController: UIHostingController { + var isNavigationBarHidden: Bool { false } + + override init(rootView: T) { + super.init(rootView: rootView) + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .clear + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + } + + @available(*, unavailable) @MainActor dynamic required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} + +enum StartFactory { + static func createStartController() -> UINavigationController { + let coordinator = StartCoordinator() + let viewModel = StartVM(coordinator: coordinator) + let controller = StartController(viewModel: viewModel) + + coordinator.router = controller + + return UINavigationController(rootViewController: controller) + } +} + +class StartCoordinator { + weak var router: UIViewController? + + func push() { + let controller = ContentFactory.createContentController() + router?.navigationController?.pushViewController(controller, animated: true) + } +} + +class StartController: HostingController { + init(viewModel: StartVM) { + super.init(rootView: StartScreen(viewModel: viewModel)) + } +} + +class StartVM: ObservableObject { + let coordinator: StartCoordinator + + init(coordinator: StartCoordinator) { + self.coordinator = coordinator + } + + func push() { + coordinator.push() + } +} + +struct StartScreen: View { + @ObservedObject var viewModel: StartVM + + var body: some View { + ZStack { + Color.white + .ignoresSafeArea() + Button { + viewModel.push() + } label: { + Text("Open") + } } } } + +enum ContentFactory { + static func createContentController() -> UIViewController { + let coordinator = ContentCoordinator() + let viewModel = ContentViewModel(coordinator: coordinator) + let controller = ContentController(viewModel: viewModel) + + coordinator.router = controller + + return controller + } +} + +class ContentCoordinator { + weak var router: UIViewController? + + func pop() { + router?.navigationController?.popViewController(animated: true) + } +} + +class ContentController: HostingController { + init(viewModel: ContentViewModel) { + super.init(rootView: ContentView(viewModel: viewModel)) + print("init ContentController") + } + + deinit { + print("deinit ContentController") + } +} diff --git a/ExampleApp/ExampleApp/SceneDelegate.swift b/ExampleApp/ExampleApp/SceneDelegate.swift new file mode 100644 index 0000000..7c84f5a --- /dev/null +++ b/ExampleApp/ExampleApp/SceneDelegate.swift @@ -0,0 +1,30 @@ +// +// SceneDelegate.swift +// Example +// +// Created by Victor Kostin on 25.11.2024. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = (scene as? UIWindowScene) else { + return + } + + window = UIWindow(windowScene: windowScene) + window?.windowScene = windowScene + window?.makeKeyAndVisible() + + let rootViewController = StartFactory.createStartController() + + window?.rootViewController = rootViewController + } +} diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index f43cd76..fc4dc09 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -22,9 +22,9 @@ public enum ErrorHideBehaviour { public struct FormView: View { @State private var fieldStates: [FieldState] = .empty @State private var currentFocusedFieldId: String = .empty - @State private var formValidator = FormValidator() +// @State private var formValidator = FormValidator() - @ViewBuilder private let content: (FormValidator) -> Content + @ViewBuilder private let content: (@escaping (Bool) -> Bool) -> Content private let errorHideBehaviour: ErrorHideBehaviour private let validationBehaviour: ValidationBehaviour @@ -32,7 +32,7 @@ public struct FormView: View { public init( validate: ValidationBehaviour = .never, hideError: ErrorHideBehaviour = .onValueChanged, - @ViewBuilder content: @escaping (FormValidator) -> Content + @ViewBuilder content: @escaping (@escaping (Bool) -> Bool) -> Content ) { self.content = content self.validationBehaviour = validate @@ -40,7 +40,7 @@ public struct FormView: View { } public var body: some View { - content(formValidator) + content(valid) .onPreferenceChange(FieldStatesKey.self) { newValue in fieldStates = newValue @@ -48,16 +48,16 @@ public struct FormView: View { currentFocusedFieldId = focusedField?.id ?? .empty // Замыкание onValidateRun вызывается методом validate() FormValidator'a. - formValidator.onValidateRun = { focusOnFirstFailedField in - let resutls = newValue.map { $0.onValidate() } - - // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { - currentFocusedFieldId = fieldStates[index].id - } - - return resutls.allSatisfy { $0 } - } +// formValidator.onValidateRun = { focusOnFirstFailedField in +// let resutls = newValue.map { $0.onValidate() } +// +// // Фокус на первом зафейленом филде. +// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { +// currentFocusedFieldId = fieldStates[index].id +// } +// +// return resutls.allSatisfy { $0 } +// } } .onSubmit(of: .text) { currentFocusedFieldId = FocusService.getNextFocusFieldId( @@ -69,4 +69,15 @@ public struct FormView: View { .environment(\.errorHideBehaviour, errorHideBehaviour) .environment(\.validationBehaviour, validationBehaviour) } + + func valid(focusOnFirstFailedField: Bool) -> Bool { + let resutls = fieldStates.map { $0.onValidate() } + + // Фокус на первом зафейленом филде. + if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + currentFocusedFieldId = fieldStates[index].id + } + + return resutls.allSatisfy { $0 } + } } From 0784eb189a1c5d3e6d00cf1edc817282d797affe Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Mon, 25 Nov 2024 16:52:46 +0300 Subject: [PATCH 02/13] Change struct to class (not fix) --- ExampleApp/ExampleApp/ContentView.swift | 6 +-- Sources/FormView/FormView.swift | 41 +++++++------------ .../Validation/Validators/FormValidator.swift | 2 +- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift index 029f792..8d44479 100644 --- a/ExampleApp/ExampleApp/ContentView.swift +++ b/ExampleApp/ExampleApp/ContentView.swift @@ -15,7 +15,7 @@ struct ContentView: View { FormView( validate: .never, hideError: .onValueChanged - ) { validate in + ) { [weak viewModel] proxy in FormField( value: $viewModel.name, rules: [ @@ -48,14 +48,14 @@ struct ContentView: View { FormField( value: $viewModel.confirmPass, rules: [ - TextValidationRule.equalTo(value: viewModel.pass, message: "Not equal to pass"), + TextValidationRule.equalTo(value: viewModel?.pass ?? "", message: "Not equal to pass"), .notEmpty(message: "Confirm pass not empty") ] ) { failedRules in SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules) } Button("Validate") { - print("Form is valid: \(proxy(true))") + print("Form is valid: \(proxy.validate())") } } .padding(.horizontal, 16) diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index fc4dc09..e9b5e75 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -22,9 +22,9 @@ public enum ErrorHideBehaviour { public struct FormView: View { @State private var fieldStates: [FieldState] = .empty @State private var currentFocusedFieldId: String = .empty -// @State private var formValidator = FormValidator() + @ObservedObject private var formValidator = FormValidator() - @ViewBuilder private let content: (@escaping (Bool) -> Bool) -> Content + @ViewBuilder private let content: (FormValidator) -> Content private let errorHideBehaviour: ErrorHideBehaviour private let validationBehaviour: ValidationBehaviour @@ -32,7 +32,7 @@ public struct FormView: View { public init( validate: ValidationBehaviour = .never, hideError: ErrorHideBehaviour = .onValueChanged, - @ViewBuilder content: @escaping (@escaping (Bool) -> Bool) -> Content + @ViewBuilder content: @escaping (FormValidator) -> Content ) { self.content = content self.validationBehaviour = validate @@ -40,24 +40,24 @@ public struct FormView: View { } public var body: some View { - content(valid) - .onPreferenceChange(FieldStatesKey.self) { newValue in + content(formValidator) + .onPreferenceChange(FieldStatesKey.self) { [weak formValidator] newValue in fieldStates = newValue let focusedField = newValue.first { $0.isFocused } currentFocusedFieldId = focusedField?.id ?? .empty // Замыкание onValidateRun вызывается методом validate() FormValidator'a. -// formValidator.onValidateRun = { focusOnFirstFailedField in -// let resutls = newValue.map { $0.onValidate() } -// -// // Фокус на первом зафейленом филде. -// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { -// currentFocusedFieldId = fieldStates[index].id -// } -// -// return resutls.allSatisfy { $0 } -// } + formValidator?.onValidateRun = { focusOnFirstFailedField in + let resutls = newValue.map { $0.onValidate() } + + // Фокус на первом зафейленом филде. + if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + currentFocusedFieldId = fieldStates[index].id + } + + return resutls.allSatisfy { $0 } + } } .onSubmit(of: .text) { currentFocusedFieldId = FocusService.getNextFocusFieldId( @@ -69,15 +69,4 @@ public struct FormView: View { .environment(\.errorHideBehaviour, errorHideBehaviour) .environment(\.validationBehaviour, validationBehaviour) } - - func valid(focusOnFirstFailedField: Bool) -> Bool { - let resutls = fieldStates.map { $0.onValidate() } - - // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { - currentFocusedFieldId = fieldStates[index].id - } - - return resutls.allSatisfy { $0 } - } } diff --git a/Sources/FormView/Validation/Validators/FormValidator.swift b/Sources/FormView/Validation/Validators/FormValidator.swift index b493a70..c95d723 100644 --- a/Sources/FormView/Validation/Validators/FormValidator.swift +++ b/Sources/FormView/Validation/Validators/FormValidator.swift @@ -7,7 +7,7 @@ import Foundation -public struct FormValidator { +public class FormValidator: ObservableObject { var onValidateRun: ((Bool) -> Bool)? public init() { From 3eed0cb3fc3d634b0dd7a9f1b7751582968c06a3 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Tue, 26 Nov 2024 14:13:25 +0300 Subject: [PATCH 03/13] WIP: add state object view model # Conflicts: # ExampleApp/ExampleApp/ContentView.swift # Sources/FormView/FormView.swift --- ExampleApp/ExampleApp/ContentView.swift | 2 +- Sources/FormView/FormField.swift | 86 +++++++++++---- Sources/FormView/FormView.swift | 135 +++++++++++++++++++----- 3 files changed, 177 insertions(+), 46 deletions(-) diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift index 8d44479..13084f2 100644 --- a/ExampleApp/ExampleApp/ContentView.swift +++ b/ExampleApp/ExampleApp/ContentView.swift @@ -15,7 +15,7 @@ struct ContentView: View { FormView( validate: .never, hideError: .onValueChanged - ) { [weak viewModel] proxy in + ) { proxy in FormField( value: $viewModel.name, rules: [ diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index a1e8d91..0b0cc54 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -7,19 +7,65 @@ import SwiftUI +class FormFieldStateHandler: ObservableObject { + @Published var failedValidationRules: [Rule] = [] + + var id: String = UUID().uuidString + + private var value: Value + private var isFocused: Bool = false + private let validator: FieldValidator + + init(value: Value, failedValidationRules: [Rule]) { + self.value = value + self.failedValidationRules = failedValidationRules + self.validator = FieldValidator(rules: failedValidationRules) + } + + func updateValue(newValue: Value) { + value = newValue + } + + func updateIsFocused(newValue: Bool) { + isFocused = newValue + } + + func updateRule() { + if let value = value as? Rule.Value { + failedValidationRules = validator.validate(value: value) + } + } + + func getFieldState() -> FieldState { + return FieldState(id: id, isFocused: isFocused) { [weak self] in + guard + let self, + let value = self.value as? Rule.Value + else { + return true + } + + print("value \(value)") + + let failedRules = validator.validate(value: value) + failedValidationRules = failedRules + + return failedRules.isEmpty + } + } +} + public struct FormField: View where Value == Rule.Value { + @StateObject var fromStateHandler: FormFieldStateHandler @Binding private var value: Value @ViewBuilder private let content: ([Rule]) -> Content - @State private var failedValidationRules: [Rule] = [] - // Fields Focus @FocusState private var isFocused: Bool - @State private var id: String = UUID().uuidString +// @State private var id: String = UUID().uuidString @Environment(\.focusedFieldId) var currentFocusedFieldId // ValidateInput - private let validator: FieldValidator @Environment(\.errorHideBehaviour) var errorHideBehaviour @Environment(\.validationBehaviour) var validationBehaviour @@ -30,50 +76,54 @@ public struct FormField: V ) { self._value = value self.content = content - self.validator = FieldValidator(rules: rules) + self._fromStateHandler = StateObject(wrappedValue: FormFieldStateHandler(value: value.wrappedValue, failedValidationRules: rules)) } public var body: some View { - content(failedValidationRules) + content(fromStateHandler.failedValidationRules) // Fields Focus .onChange(of: currentFocusedFieldId) { newValue in DispatchQueue.main.async { - isFocused = newValue.trimmingCharacters(in: .whitespaces) == id + isFocused = newValue.trimmingCharacters(in: .whitespaces) == fromStateHandler.id } } .preference( key: FieldStatesKey.self, value: [ // Замыкание для каждого филда вызывается FormValidator'ом из FormView для валидации по требованию - FieldState(id: id, isFocused: isFocused) { - let failedRules = validator.validate(value: value) - failedValidationRules = failedRules - - return failedRules.isEmpty - } + fromStateHandler.getFieldState() ] ) .focused($isFocused) // Fields Validation .onChange(of: value) { newValue in + fromStateHandler.updateValue(newValue: newValue) + if errorHideBehaviour == .onValueChanged { - failedValidationRules = .empty + fromStateHandler.failedValidationRules = .empty + print("==--== 1") } if validationBehaviour == .onFieldValueChanged { - failedValidationRules = validator.validate(value: newValue) + fromStateHandler.updateRule() + print("==--== 2") } } .onChange(of: isFocused) { newValue in + fromStateHandler.updateIsFocused(newValue: newValue) + if errorHideBehaviour == .onFocusLost && newValue == false { - failedValidationRules = .empty + fromStateHandler.failedValidationRules = .empty + print("==--== 3") } else if errorHideBehaviour == .onFocus && newValue == true { - failedValidationRules = .empty + fromStateHandler.failedValidationRules = .empty + print("==--== 4") } if validationBehaviour == .onFieldFocusLost && newValue == false { - failedValidationRules = validator.validate(value: value) + fromStateHandler.updateRule() + print("==--== 5") } } } diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index e9b5e75..304c6ab 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -19,11 +19,110 @@ public enum ErrorHideBehaviour { case onFocusLost } -public struct FormView: View { - @State private var fieldStates: [FieldState] = .empty - @State private var currentFocusedFieldId: String = .empty - @ObservedObject private var formValidator = FormValidator() +//public struct FormView: View { +// @State private var fieldStates: [FieldState] = .empty +// @State private var currentFocusedFieldId: String = .empty +// @State private var formValidator = FormValidator() +// +// @ViewBuilder private let content: (@escaping (Bool) -> Bool) -> Content +// +// private let errorHideBehaviour: ErrorHideBehaviour +// private let validationBehaviour: ValidationBehaviour +// +// public init( +// validate: ValidationBehaviour = .never, +// hideError: ErrorHideBehaviour = .onValueChanged, +// @ViewBuilder content: @escaping (@escaping (Bool) -> Bool) -> Content +// ) { +// self.content = content +// self.validationBehaviour = validate +// self.errorHideBehaviour = hideError +// } +// +// public var body: some View { +// content(valid) +// .onPreferenceChange(FieldStatesKey.self) { newValue in +// fieldStates = newValue +// +// let focusedField = newValue.first { $0.isFocused } +// currentFocusedFieldId = focusedField?.id ?? .empty +// +// // Замыкание onValidateRun вызывается методом validate() FormValidator'a. +// formValidator.onValidateRun = { focusOnFirstFailedField in +// let resutls = newValue.map { $0.onValidate() } +// +// // Фокус на первом зафейленом филде. +// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { +// // Вот из за этой строки +// currentFocusedFieldId = fieldStates[index].id +// } +// +// return resutls.allSatisfy { $0 } +// } +// } +// .onSubmit(of: .text) { +// currentFocusedFieldId = FocusService.getNextFocusFieldId( +// states: fieldStates, +// currentFocusField: currentFocusedFieldId +// ) +// } +// .environment(\.focusedFieldId, currentFocusedFieldId) +// .environment(\.errorHideBehaviour, errorHideBehaviour) +// .environment(\.validationBehaviour, validationBehaviour) +// } +// +// func valid(focusOnFirstFailedField: Bool) -> Bool { +// let resutls = fieldStates.map { $0.onValidate() } +// +// // Фокус на первом зафейленом филде. +// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { +// // TODO: если закоментировать эту строку которая после валидации выбирает активный филд +// // утечка починится +// currentFocusedFieldId = fieldStates[index].id +// } +// +// return resutls.allSatisfy { $0 } +// } +//} + +private class FormStateHandler: ObservableObject { + @Published var fieldStates: [FieldState] = .empty + @Published var currentFocusedFieldId: String = .empty + @Published var formValidator = FormValidator() + + func updateFieldStates(newStates: [FieldState]) { + fieldStates = newStates + + let focusedField = newStates.first { $0.isFocused } + currentFocusedFieldId = focusedField?.id ?? .empty + + // Замыкание onValidateRun вызывается методом validate() FormValidator'a. + formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in + guard let self else { + return false + } + + let resutls = newStates.map { $0.onValidate() } + + // Фокус на первом зафейленом филде. + if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + currentFocusedFieldId = fieldStates[index].id + } + + return resutls.allSatisfy { $0 } + } + } + func submit() { + currentFocusedFieldId = FocusService.getNextFocusFieldId( + states: fieldStates, + currentFocusField: currentFocusedFieldId + ) + } +} + +public struct FormView: View { + @StateObject private var formStateHandler = FormStateHandler() @ViewBuilder private let content: (FormValidator) -> Content private let errorHideBehaviour: ErrorHideBehaviour @@ -40,32 +139,14 @@ public struct FormView: View { } public var body: some View { - content(formValidator) - .onPreferenceChange(FieldStatesKey.self) { [weak formValidator] newValue in - fieldStates = newValue - - let focusedField = newValue.first { $0.isFocused } - currentFocusedFieldId = focusedField?.id ?? .empty - - // Замыкание onValidateRun вызывается методом validate() FormValidator'a. - formValidator?.onValidateRun = { focusOnFirstFailedField in - let resutls = newValue.map { $0.onValidate() } - - // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { - currentFocusedFieldId = fieldStates[index].id - } - - return resutls.allSatisfy { $0 } - } + return content(formStateHandler.formValidator) + .onPreferenceChange(FieldStatesKey.self) { newStates in + formStateHandler.updateFieldStates(newStates: newStates) } .onSubmit(of: .text) { - currentFocusedFieldId = FocusService.getNextFocusFieldId( - states: fieldStates, - currentFocusField: currentFocusedFieldId - ) + formStateHandler.submit() } - .environment(\.focusedFieldId, currentFocusedFieldId) + .environment(\.focusedFieldId, formStateHandler.currentFocusedFieldId) .environment(\.errorHideBehaviour, errorHideBehaviour) .environment(\.validationBehaviour, validationBehaviour) } From e661f304300e81f8bc3a288797c72fc839a273aa Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Tue, 26 Nov 2024 14:33:35 +0300 Subject: [PATCH 04/13] WIP: fix memory leak --- ExampleApp/ExampleApp/ContentView.swift | 2 +- Sources/FormView/FormField.swift | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift index 13084f2..c4513fa 100644 --- a/ExampleApp/ExampleApp/ContentView.swift +++ b/ExampleApp/ExampleApp/ContentView.swift @@ -48,7 +48,7 @@ struct ContentView: View { FormField( value: $viewModel.confirmPass, rules: [ - TextValidationRule.equalTo(value: viewModel?.pass ?? "", message: "Not equal to pass"), + TextValidationRule.equalTo(value: viewModel.pass, message: "Not equal to pass"), .notEmpty(message: "Confirm pass not empty") ] ) { failedRules in diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index 0b0cc54..d5b85ce 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -8,17 +8,18 @@ import SwiftUI class FormFieldStateHandler: ObservableObject { - @Published var failedValidationRules: [Rule] = [] - - var id: String = UUID().uuidString + var failedValidationRules: [Rule] = [] private var value: Value private var isFocused: Bool = false + + private let id: String private let validator: FieldValidator - init(value: Value, failedValidationRules: [Rule]) { + init(value: Value, failedValidationRules: [Rule], id: String) { self.value = value self.failedValidationRules = failedValidationRules + self.id = id self.validator = FieldValidator(rules: failedValidationRules) } @@ -62,21 +63,24 @@ public struct FormField: V // Fields Focus @FocusState private var isFocused: Bool -// @State private var id: String = UUID().uuidString @Environment(\.focusedFieldId) var currentFocusedFieldId // ValidateInput @Environment(\.errorHideBehaviour) var errorHideBehaviour @Environment(\.validationBehaviour) var validationBehaviour + private var id: String + public init( value: Binding, rules: [Rule] = [], + id: String = UUID().uuidString, @ViewBuilder content: @escaping ([Rule]) -> Content ) { self._value = value self.content = content - self._fromStateHandler = StateObject(wrappedValue: FormFieldStateHandler(value: value.wrappedValue, failedValidationRules: rules)) + self.id = id + self._fromStateHandler = StateObject(wrappedValue: FormFieldStateHandler(value: value.wrappedValue, failedValidationRules: rules, id: id)) } public var body: some View { @@ -84,7 +88,7 @@ public struct FormField: V // Fields Focus .onChange(of: currentFocusedFieldId) { newValue in DispatchQueue.main.async { - isFocused = newValue.trimmingCharacters(in: .whitespaces) == fromStateHandler.id + isFocused = newValue.trimmingCharacters(in: .whitespaces) == id } } .preference( From f1759baea4ac2e25b80a14768f7d0da7265fee8e Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 14:42:19 +0300 Subject: [PATCH 05/13] Fix memory leak --- Sources/FormView/FormField.swift | 88 +++----------- Sources/FormView/FormView.swift | 110 +----------------- .../FormView/Handler/FormStateHandler.swift | 44 +++++++ 3 files changed, 65 insertions(+), 177 deletions(-) create mode 100644 Sources/FormView/Handler/FormStateHandler.swift diff --git a/Sources/FormView/FormField.swift b/Sources/FormView/FormField.swift index d5b85ce..a1e8d91 100644 --- a/Sources/FormView/FormField.swift +++ b/Sources/FormView/FormField.swift @@ -7,84 +7,34 @@ import SwiftUI -class FormFieldStateHandler: ObservableObject { - var failedValidationRules: [Rule] = [] - - private var value: Value - private var isFocused: Bool = false - - private let id: String - private let validator: FieldValidator - - init(value: Value, failedValidationRules: [Rule], id: String) { - self.value = value - self.failedValidationRules = failedValidationRules - self.id = id - self.validator = FieldValidator(rules: failedValidationRules) - } - - func updateValue(newValue: Value) { - value = newValue - } - - func updateIsFocused(newValue: Bool) { - isFocused = newValue - } - - func updateRule() { - if let value = value as? Rule.Value { - failedValidationRules = validator.validate(value: value) - } - } - - func getFieldState() -> FieldState { - return FieldState(id: id, isFocused: isFocused) { [weak self] in - guard - let self, - let value = self.value as? Rule.Value - else { - return true - } - - print("value \(value)") - - let failedRules = validator.validate(value: value) - failedValidationRules = failedRules - - return failedRules.isEmpty - } - } -} - public struct FormField: View where Value == Rule.Value { - @StateObject var fromStateHandler: FormFieldStateHandler @Binding private var value: Value @ViewBuilder private let content: ([Rule]) -> Content + @State private var failedValidationRules: [Rule] = [] + // Fields Focus @FocusState private var isFocused: Bool + @State private var id: String = UUID().uuidString @Environment(\.focusedFieldId) var currentFocusedFieldId // ValidateInput + private let validator: FieldValidator @Environment(\.errorHideBehaviour) var errorHideBehaviour @Environment(\.validationBehaviour) var validationBehaviour - private var id: String - public init( value: Binding, rules: [Rule] = [], - id: String = UUID().uuidString, @ViewBuilder content: @escaping ([Rule]) -> Content ) { self._value = value self.content = content - self.id = id - self._fromStateHandler = StateObject(wrappedValue: FormFieldStateHandler(value: value.wrappedValue, failedValidationRules: rules, id: id)) + self.validator = FieldValidator(rules: rules) } public var body: some View { - content(fromStateHandler.failedValidationRules) + content(failedValidationRules) // Fields Focus .onChange(of: currentFocusedFieldId) { newValue in DispatchQueue.main.async { @@ -95,39 +45,35 @@ public struct FormField: V key: FieldStatesKey.self, value: [ // Замыкание для каждого филда вызывается FormValidator'ом из FormView для валидации по требованию - fromStateHandler.getFieldState() + FieldState(id: id, isFocused: isFocused) { + let failedRules = validator.validate(value: value) + failedValidationRules = failedRules + + return failedRules.isEmpty + } ] ) .focused($isFocused) // Fields Validation .onChange(of: value) { newValue in - fromStateHandler.updateValue(newValue: newValue) - if errorHideBehaviour == .onValueChanged { - fromStateHandler.failedValidationRules = .empty - print("==--== 1") + failedValidationRules = .empty } if validationBehaviour == .onFieldValueChanged { - fromStateHandler.updateRule() - print("==--== 2") + failedValidationRules = validator.validate(value: newValue) } } .onChange(of: isFocused) { newValue in - fromStateHandler.updateIsFocused(newValue: newValue) - if errorHideBehaviour == .onFocusLost && newValue == false { - fromStateHandler.failedValidationRules = .empty - print("==--== 3") + failedValidationRules = .empty } else if errorHideBehaviour == .onFocus && newValue == true { - fromStateHandler.failedValidationRules = .empty - print("==--== 4") + failedValidationRules = .empty } if validationBehaviour == .onFieldFocusLost && newValue == false { - fromStateHandler.updateRule() - print("==--== 5") + failedValidationRules = validator.validate(value: value) } } } diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 304c6ab..2d44499 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -19,108 +19,6 @@ public enum ErrorHideBehaviour { case onFocusLost } -//public struct FormView: View { -// @State private var fieldStates: [FieldState] = .empty -// @State private var currentFocusedFieldId: String = .empty -// @State private var formValidator = FormValidator() -// -// @ViewBuilder private let content: (@escaping (Bool) -> Bool) -> Content -// -// private let errorHideBehaviour: ErrorHideBehaviour -// private let validationBehaviour: ValidationBehaviour -// -// public init( -// validate: ValidationBehaviour = .never, -// hideError: ErrorHideBehaviour = .onValueChanged, -// @ViewBuilder content: @escaping (@escaping (Bool) -> Bool) -> Content -// ) { -// self.content = content -// self.validationBehaviour = validate -// self.errorHideBehaviour = hideError -// } -// -// public var body: some View { -// content(valid) -// .onPreferenceChange(FieldStatesKey.self) { newValue in -// fieldStates = newValue -// -// let focusedField = newValue.first { $0.isFocused } -// currentFocusedFieldId = focusedField?.id ?? .empty -// -// // Замыкание onValidateRun вызывается методом validate() FormValidator'a. -// formValidator.onValidateRun = { focusOnFirstFailedField in -// let resutls = newValue.map { $0.onValidate() } -// -// // Фокус на первом зафейленом филде. -// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { -// // Вот из за этой строки -// currentFocusedFieldId = fieldStates[index].id -// } -// -// return resutls.allSatisfy { $0 } -// } -// } -// .onSubmit(of: .text) { -// currentFocusedFieldId = FocusService.getNextFocusFieldId( -// states: fieldStates, -// currentFocusField: currentFocusedFieldId -// ) -// } -// .environment(\.focusedFieldId, currentFocusedFieldId) -// .environment(\.errorHideBehaviour, errorHideBehaviour) -// .environment(\.validationBehaviour, validationBehaviour) -// } -// -// func valid(focusOnFirstFailedField: Bool) -> Bool { -// let resutls = fieldStates.map { $0.onValidate() } -// -// // Фокус на первом зафейленом филде. -// if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { -// // TODO: если закоментировать эту строку которая после валидации выбирает активный филд -// // утечка починится -// currentFocusedFieldId = fieldStates[index].id -// } -// -// return resutls.allSatisfy { $0 } -// } -//} - -private class FormStateHandler: ObservableObject { - @Published var fieldStates: [FieldState] = .empty - @Published var currentFocusedFieldId: String = .empty - @Published var formValidator = FormValidator() - - func updateFieldStates(newStates: [FieldState]) { - fieldStates = newStates - - let focusedField = newStates.first { $0.isFocused } - currentFocusedFieldId = focusedField?.id ?? .empty - - // Замыкание onValidateRun вызывается методом validate() FormValidator'a. - formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in - guard let self else { - return false - } - - let resutls = newStates.map { $0.onValidate() } - - // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { - currentFocusedFieldId = fieldStates[index].id - } - - return resutls.allSatisfy { $0 } - } - } - - func submit() { - currentFocusedFieldId = FocusService.getNextFocusFieldId( - states: fieldStates, - currentFocusField: currentFocusedFieldId - ) - } -} - public struct FormView: View { @StateObject private var formStateHandler = FormStateHandler() @ViewBuilder private let content: (FormValidator) -> Content @@ -140,11 +38,11 @@ public struct FormView: View { public var body: some View { return content(formStateHandler.formValidator) - .onPreferenceChange(FieldStatesKey.self) { newStates in - formStateHandler.updateFieldStates(newStates: newStates) + .onPreferenceChange(FieldStatesKey.self) { [weak formStateHandler] newStates in + formStateHandler?.updateFieldStates(newStates: newStates) } - .onSubmit(of: .text) { - formStateHandler.submit() + .onSubmit(of: .text) { [weak formStateHandler] in + formStateHandler?.submit() } .environment(\.focusedFieldId, formStateHandler.currentFocusedFieldId) .environment(\.errorHideBehaviour, errorHideBehaviour) diff --git a/Sources/FormView/Handler/FormStateHandler.swift b/Sources/FormView/Handler/FormStateHandler.swift new file mode 100644 index 0000000..8f6b25d --- /dev/null +++ b/Sources/FormView/Handler/FormStateHandler.swift @@ -0,0 +1,44 @@ +// +// FormStateHandler.swift +// +// +// Created by Victor Kostin on 27.11.2024. +// + +import SwiftUI + +class FormStateHandler: ObservableObject { + @Published var fieldStates: [FieldState] = .empty + @Published var currentFocusedFieldId: String = .empty + @Published var formValidator = FormValidator() + + func updateFieldStates(newStates: [FieldState]) { + fieldStates = newStates + + let focusedField = newStates.first { $0.isFocused } + currentFocusedFieldId = focusedField?.id ?? .empty + + // Замыкание onValidateRun вызывается методом validate() FormValidator'a. + formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in + guard let self else { + return false + } + + let resutls = newStates.map { $0.onValidate() } + + // Фокус на первом зафейленом филде. + if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + currentFocusedFieldId = fieldStates[index].id + } + + return resutls.allSatisfy { $0 } + } + } + + func submit() { + currentFocusedFieldId = FocusService.getNextFocusFieldId( + states: fieldStates, + currentFocusField: currentFocusedFieldId + ) + } +} From 5504c9ce6c50c9f14d527344b100891f7a8964a0 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 18:30:00 +0300 Subject: [PATCH 06/13] Change example --- ExampleApp/Example.xcodeproj/project.pbxproj | 78 +++++++++++- .../ExampleApp/Base/HostingController.swift | 32 +++++ ExampleApp/ExampleApp/ExampleApp.swift | 118 ------------------ .../UI/ContentScreen/ContentController.swift | 19 +++ .../UI/ContentScreen/ContentCoordinator.swift | 16 +++ .../UI/ContentScreen/ContentFactory.swift | 20 +++ .../{ => UI/ContentScreen}/ContentView.swift | 0 .../ContentScreen}/ContentViewModel.swift | 0 .../UI/StartScreen/StartController.swift | 14 +++ .../UI/StartScreen/StartCoordinator.swift | 17 +++ .../UI/StartScreen/StartFactory.swift | 20 +++ .../ExampleApp/UI/StartScreen/StartView.swift | 24 ++++ .../UI/StartScreen/StartViewModel.swift | 20 +++ Sources/FormView/FormView.swift | 2 + 14 files changed, 256 insertions(+), 124 deletions(-) create mode 100644 ExampleApp/ExampleApp/Base/HostingController.swift delete mode 100644 ExampleApp/ExampleApp/ExampleApp.swift create mode 100644 ExampleApp/ExampleApp/UI/ContentScreen/ContentController.swift create mode 100644 ExampleApp/ExampleApp/UI/ContentScreen/ContentCoordinator.swift create mode 100644 ExampleApp/ExampleApp/UI/ContentScreen/ContentFactory.swift rename ExampleApp/ExampleApp/{ => UI/ContentScreen}/ContentView.swift (100%) rename ExampleApp/ExampleApp/{ => UI/ContentScreen}/ContentViewModel.swift (100%) create mode 100644 ExampleApp/ExampleApp/UI/StartScreen/StartController.swift create mode 100644 ExampleApp/ExampleApp/UI/StartScreen/StartCoordinator.swift create mode 100644 ExampleApp/ExampleApp/UI/StartScreen/StartFactory.swift create mode 100644 ExampleApp/ExampleApp/UI/StartScreen/StartView.swift create mode 100644 ExampleApp/ExampleApp/UI/StartScreen/StartViewModel.swift diff --git a/ExampleApp/Example.xcodeproj/project.pbxproj b/ExampleApp/Example.xcodeproj/project.pbxproj index 6ca9530..67e14ec 100644 --- a/ExampleApp/Example.xcodeproj/project.pbxproj +++ b/ExampleApp/Example.xcodeproj/project.pbxproj @@ -9,11 +9,19 @@ /* Begin PBXBuildFile section */ 4211F22E2999730F00D13FD0 /* TextInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4211F22D2999730F00D13FD0 /* TextInputField.swift */; }; 426A30E529E81D5A00C5FB02 /* MyRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426A30E429E81D5A00C5FB02 /* MyRule.swift */; }; - 426CF63829850D1A00012FBE /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426CF63729850D1A00012FBE /* ExampleApp.swift */; }; 426CF63A29850D1A00012FBE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426CF63929850D1A00012FBE /* ContentView.swift */; }; 426CF63C29850D1B00012FBE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 426CF63B29850D1B00012FBE /* Assets.xcassets */; }; 426CF63F29850D1B00012FBE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 426CF63E29850D1B00012FBE /* Preview Assets.xcassets */; }; 426CF64929850DD400012FBE /* FormView in Frameworks */ = {isa = PBXBuildFile; productRef = 426CF64829850DD400012FBE /* FormView */; }; + 7392F4562CF76C8900331B40 /* HostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F4552CF76C8900331B40 /* HostingController.swift */; }; + 7392F45A2CF76D7600331B40 /* StartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F4592CF76D7500331B40 /* StartFactory.swift */; }; + 7392F45C2CF76FFB00331B40 /* StartCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F45B2CF76FFA00331B40 /* StartCoordinator.swift */; }; + 7392F45E2CF7701B00331B40 /* StartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F45D2CF7701A00331B40 /* StartController.swift */; }; + 7392F4602CF7709D00331B40 /* StartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F45F2CF7709D00331B40 /* StartView.swift */; }; + 7392F4632CF7710900331B40 /* StartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F4622CF7710800331B40 /* StartViewModel.swift */; }; + 7392F4652CF7717E00331B40 /* ContentFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F4642CF7717D00331B40 /* ContentFactory.swift */; }; + 7392F4692CF771AB00331B40 /* ContentCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F4682CF771AB00331B40 /* ContentCoordinator.swift */; }; + 7392F46B2CF771C900331B40 /* ContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7392F46A2CF771C900331B40 /* ContentController.swift */; }; 739E30032CF4A1AF009B795F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739E30022CF4A1AB009B795F /* AppDelegate.swift */; }; 739E30052CF4A1CF009B795F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739E30042CF4A1CE009B795F /* SceneDelegate.swift */; }; E1DB5A1F2A73BFCF0024C47A /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */; }; @@ -24,12 +32,20 @@ 4211F22D2999730F00D13FD0 /* TextInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputField.swift; sourceTree = ""; }; 426A30E429E81D5A00C5FB02 /* MyRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyRule.swift; sourceTree = ""; }; 426CF63429850D1A00012FBE /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 426CF63729850D1A00012FBE /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 426CF63929850D1A00012FBE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 426CF63B29850D1B00012FBE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 426CF63E29850D1B00012FBE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 426CF64629850D9F00012FBE /* FormView */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FormView; path = ..; sourceTree = ""; }; 426CF64C29903DBC00012FBE /* Example-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Example-Info.plist"; sourceTree = ""; }; + 7392F4552CF76C8900331B40 /* HostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingController.swift; sourceTree = ""; }; + 7392F4592CF76D7500331B40 /* StartFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartFactory.swift; sourceTree = ""; }; + 7392F45B2CF76FFA00331B40 /* StartCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCoordinator.swift; sourceTree = ""; }; + 7392F45D2CF7701A00331B40 /* StartController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartController.swift; sourceTree = ""; }; + 7392F45F2CF7709D00331B40 /* StartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartView.swift; sourceTree = ""; }; + 7392F4622CF7710800331B40 /* StartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartViewModel.swift; sourceTree = ""; }; + 7392F4642CF7717D00331B40 /* ContentFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFactory.swift; sourceTree = ""; }; + 7392F4682CF771AB00331B40 /* ContentCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentCoordinator.swift; sourceTree = ""; }; + 7392F46A2CF771C900331B40 /* ContentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentController.swift; sourceTree = ""; }; 739E30022CF4A1AB009B795F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 739E30042CF4A1CE009B795F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; @@ -72,10 +88,9 @@ children = ( 739E30022CF4A1AB009B795F /* AppDelegate.swift */, 739E30042CF4A1CE009B795F /* SceneDelegate.swift */, - 426CF63729850D1A00012FBE /* ExampleApp.swift */, - 426CF63929850D1A00012FBE /* ContentView.swift */, - E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */, 426A30E429E81D5A00C5FB02 /* MyRule.swift */, + 7392F4542CF76C7400331B40 /* Base */, + 7392F4572CF76CB500331B40 /* UI */, E1E8FCDC2A52C61D0099A852 /* InputFields */, 426CF63B29850D1B00012FBE /* Assets.xcassets */, 426CF63D29850D1B00012FBE /* Preview Content */, @@ -106,6 +121,47 @@ name = Frameworks; sourceTree = ""; }; + 7392F4542CF76C7400331B40 /* Base */ = { + isa = PBXGroup; + children = ( + 7392F4552CF76C8900331B40 /* HostingController.swift */, + ); + path = Base; + sourceTree = ""; + }; + 7392F4572CF76CB500331B40 /* UI */ = { + isa = PBXGroup; + children = ( + 7392F4612CF770E100331B40 /* ContentScreen */, + 7392F4582CF76D6300331B40 /* StartScreen */, + ); + path = UI; + sourceTree = ""; + }; + 7392F4582CF76D6300331B40 /* StartScreen */ = { + isa = PBXGroup; + children = ( + 7392F4592CF76D7500331B40 /* StartFactory.swift */, + 7392F4622CF7710800331B40 /* StartViewModel.swift */, + 7392F45B2CF76FFA00331B40 /* StartCoordinator.swift */, + 7392F45D2CF7701A00331B40 /* StartController.swift */, + 7392F45F2CF7709D00331B40 /* StartView.swift */, + ); + path = StartScreen; + sourceTree = ""; + }; + 7392F4612CF770E100331B40 /* ContentScreen */ = { + isa = PBXGroup; + children = ( + E1DB5A1E2A73BFCF0024C47A /* ContentViewModel.swift */, + 7392F4642CF7717D00331B40 /* ContentFactory.swift */, + 426CF63929850D1A00012FBE /* ContentView.swift */, + 7392F46A2CF771C900331B40 /* ContentController.swift */, + 7392F4682CF771AB00331B40 /* ContentCoordinator.swift */, + ); + path = ContentScreen; + sourceTree = ""; + }; E1E8FCDC2A52C61D0099A852 /* InputFields */ = { isa = PBXGroup; children = ( @@ -212,12 +268,20 @@ buildActionMask = 2147483647; files = ( 4211F22E2999730F00D13FD0 /* TextInputField.swift in Sources */, + 7392F46B2CF771C900331B40 /* ContentController.swift in Sources */, E1E8FCDB2A52B8CD0099A852 /* SecureInputField.swift in Sources */, + 7392F4692CF771AB00331B40 /* ContentCoordinator.swift in Sources */, + 7392F45E2CF7701B00331B40 /* StartController.swift in Sources */, E1DB5A1F2A73BFCF0024C47A /* ContentViewModel.swift in Sources */, + 7392F45C2CF76FFB00331B40 /* StartCoordinator.swift in Sources */, + 7392F4602CF7709D00331B40 /* StartView.swift in Sources */, + 7392F45A2CF76D7600331B40 /* StartFactory.swift in Sources */, + 7392F4652CF7717E00331B40 /* ContentFactory.swift in Sources */, + 7392F4632CF7710900331B40 /* StartViewModel.swift in Sources */, 426A30E529E81D5A00C5FB02 /* MyRule.swift in Sources */, 739E30032CF4A1AF009B795F /* AppDelegate.swift in Sources */, 426CF63A29850D1A00012FBE /* ContentView.swift in Sources */, - 426CF63829850D1A00012FBE /* ExampleApp.swift in Sources */, + 7392F4562CF76C8900331B40 /* HostingController.swift in Sources */, 739E30052CF4A1CF009B795F /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -355,6 +419,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -384,6 +449,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ExampleApp/ExampleApp/Base/HostingController.swift b/ExampleApp/ExampleApp/Base/HostingController.swift new file mode 100644 index 0000000..14baef5 --- /dev/null +++ b/ExampleApp/ExampleApp/Base/HostingController.swift @@ -0,0 +1,32 @@ +// +// HostingController.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import SwiftUI +import UIKit + +class HostingController: UIHostingController { + var isNavigationBarHidden: Bool { false } + + override init(rootView: T) { + super.init(rootView: rootView) + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .clear + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + } + + @available(*, unavailable) @MainActor dynamic required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} diff --git a/ExampleApp/ExampleApp/ExampleApp.swift b/ExampleApp/ExampleApp/ExampleApp.swift deleted file mode 100644 index a6fbc8a..0000000 --- a/ExampleApp/ExampleApp/ExampleApp.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// ExampleApp.swift -// Example -// -// Created by Maxim Aliev on 28.01.2023. -// - -import SwiftUI -import UIKit - -class HostingController: UIHostingController { - var isNavigationBarHidden: Bool { false } - - override init(rootView: T) { - super.init(rootView: rootView) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .clear - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - } - - @available(*, unavailable) @MainActor dynamic required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } -} - -enum StartFactory { - static func createStartController() -> UINavigationController { - let coordinator = StartCoordinator() - let viewModel = StartVM(coordinator: coordinator) - let controller = StartController(viewModel: viewModel) - - coordinator.router = controller - - return UINavigationController(rootViewController: controller) - } -} - -class StartCoordinator { - weak var router: UIViewController? - - func push() { - let controller = ContentFactory.createContentController() - router?.navigationController?.pushViewController(controller, animated: true) - } -} - -class StartController: HostingController { - init(viewModel: StartVM) { - super.init(rootView: StartScreen(viewModel: viewModel)) - } -} - -class StartVM: ObservableObject { - let coordinator: StartCoordinator - - init(coordinator: StartCoordinator) { - self.coordinator = coordinator - } - - func push() { - coordinator.push() - } -} - -struct StartScreen: View { - @ObservedObject var viewModel: StartVM - - var body: some View { - ZStack { - Color.white - .ignoresSafeArea() - Button { - viewModel.push() - } label: { - Text("Open") - } - } - } -} - -enum ContentFactory { - static func createContentController() -> UIViewController { - let coordinator = ContentCoordinator() - let viewModel = ContentViewModel(coordinator: coordinator) - let controller = ContentController(viewModel: viewModel) - - coordinator.router = controller - - return controller - } -} - -class ContentCoordinator { - weak var router: UIViewController? - - func pop() { - router?.navigationController?.popViewController(animated: true) - } -} - -class ContentController: HostingController { - init(viewModel: ContentViewModel) { - super.init(rootView: ContentView(viewModel: viewModel)) - print("init ContentController") - } - - deinit { - print("deinit ContentController") - } -} diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentController.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentController.swift new file mode 100644 index 0000000..439098e --- /dev/null +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentController.swift @@ -0,0 +1,19 @@ +// +// ContentController.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import SwiftUI + +class ContentController: HostingController { + init(viewModel: ContentViewModel) { + super.init(rootView: ContentView(viewModel: viewModel)) + print("init ContentController") + } + + deinit { + print("deinit ContentController") + } +} diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentCoordinator.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentCoordinator.swift new file mode 100644 index 0000000..7ad9271 --- /dev/null +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentCoordinator.swift @@ -0,0 +1,16 @@ +// +// ContentCoordinator.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import UIKit + +class ContentCoordinator { + weak var router: UIViewController? + + func pop() { + router?.navigationController?.popViewController(animated: true) + } +} diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentFactory.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentFactory.swift new file mode 100644 index 0000000..bdac22e --- /dev/null +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentFactory.swift @@ -0,0 +1,20 @@ +// +// ContentFactory.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import UIKit + +enum ContentFactory { + static func createContentController() -> UIViewController { + let coordinator = ContentCoordinator() + let viewModel = ContentViewModel(coordinator: coordinator) + let controller = ContentController(viewModel: viewModel) + + coordinator.router = controller + + return controller + } +} diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift similarity index 100% rename from ExampleApp/ExampleApp/ContentView.swift rename to ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift diff --git a/ExampleApp/ExampleApp/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift similarity index 100% rename from ExampleApp/ExampleApp/ContentViewModel.swift rename to ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift diff --git a/ExampleApp/ExampleApp/UI/StartScreen/StartController.swift b/ExampleApp/ExampleApp/UI/StartScreen/StartController.swift new file mode 100644 index 0000000..8b55e11 --- /dev/null +++ b/ExampleApp/ExampleApp/UI/StartScreen/StartController.swift @@ -0,0 +1,14 @@ +// +// StartController.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import UIKit + +class StartController: HostingController { + init(viewModel: StartViewModel) { + super.init(rootView: StartView(viewModel: viewModel)) + } +} diff --git a/ExampleApp/ExampleApp/UI/StartScreen/StartCoordinator.swift b/ExampleApp/ExampleApp/UI/StartScreen/StartCoordinator.swift new file mode 100644 index 0000000..0a20778 --- /dev/null +++ b/ExampleApp/ExampleApp/UI/StartScreen/StartCoordinator.swift @@ -0,0 +1,17 @@ +// +// StartCoordinator.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import UIKit + +class StartCoordinator { + weak var router: UIViewController? + + func push() { + let controller = ContentFactory.createContentController() + router?.navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/ExampleApp/ExampleApp/UI/StartScreen/StartFactory.swift b/ExampleApp/ExampleApp/UI/StartScreen/StartFactory.swift new file mode 100644 index 0000000..31df1d8 --- /dev/null +++ b/ExampleApp/ExampleApp/UI/StartScreen/StartFactory.swift @@ -0,0 +1,20 @@ +// +// StartFactory.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import UIKit + +enum StartFactory { + static func createStartController() -> UINavigationController { + let coordinator = StartCoordinator() + let viewModel = StartViewModel(coordinator: coordinator) + let controller = StartController(viewModel: viewModel) + + coordinator.router = controller + + return UINavigationController(rootViewController: controller) + } +} diff --git a/ExampleApp/ExampleApp/UI/StartScreen/StartView.swift b/ExampleApp/ExampleApp/UI/StartScreen/StartView.swift new file mode 100644 index 0000000..1bb9731 --- /dev/null +++ b/ExampleApp/ExampleApp/UI/StartScreen/StartView.swift @@ -0,0 +1,24 @@ +// +// StartView.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import SwiftUI + +struct StartView: View { + @ObservedObject var viewModel: StartViewModel + + var body: some View { + ZStack { + Color.white + .ignoresSafeArea() + Button { + viewModel.push() + } label: { + Text("Open") + } + } + } +} diff --git a/ExampleApp/ExampleApp/UI/StartScreen/StartViewModel.swift b/ExampleApp/ExampleApp/UI/StartScreen/StartViewModel.swift new file mode 100644 index 0000000..1fab47c --- /dev/null +++ b/ExampleApp/ExampleApp/UI/StartScreen/StartViewModel.swift @@ -0,0 +1,20 @@ +// +// StartViewModel.swift +// Example +// +// Created by Victor Kostin on 27.11.2024. +// + +import Foundation + +class StartViewModel: ObservableObject { + let coordinator: StartCoordinator + + init(coordinator: StartCoordinator) { + self.coordinator = coordinator + } + + func push() { + coordinator.push() + } +} diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 2d44499..9e71033 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -38,6 +38,8 @@ public struct FormView: View { public var body: some View { return content(formStateHandler.formValidator) + // [weak formStateHandler] необходимо для избежания захвата сильных ссылок между + // замыканием и @StateObject .onPreferenceChange(FieldStatesKey.self) { [weak formStateHandler] newStates in formStateHandler?.updateFieldStates(newStates: newStates) } From 230d3544a5cbd4d5442252f99a1036f0b293d2ce Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 18:32:53 +0300 Subject: [PATCH 07/13] Reverte target version --- ExampleApp/Example.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ExampleApp/Example.xcodeproj/project.pbxproj b/ExampleApp/Example.xcodeproj/project.pbxproj index 67e14ec..1275b2c 100644 --- a/ExampleApp/Example.xcodeproj/project.pbxproj +++ b/ExampleApp/Example.xcodeproj/project.pbxproj @@ -419,7 +419,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -449,7 +449,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 282c7ff0102fa776e8ca51e055381b088c7dd9b4 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 18:35:08 +0300 Subject: [PATCH 08/13] Remove target version --- ExampleApp/Example.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/ExampleApp/Example.xcodeproj/project.pbxproj b/ExampleApp/Example.xcodeproj/project.pbxproj index 1275b2c..452db4c 100644 --- a/ExampleApp/Example.xcodeproj/project.pbxproj +++ b/ExampleApp/Example.xcodeproj/project.pbxproj @@ -419,7 +419,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -449,7 +448,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 308418b6cab2dc45aa3c7b902fa7183c549b764a Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 18:37:11 +0300 Subject: [PATCH 09/13] Add private --- ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift index 6816ecd..a05e288 100644 --- a/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift +++ b/ExampleApp/ExampleApp/UI/ContentScreen/ContentViewModel.swift @@ -13,7 +13,7 @@ class ContentViewModel: ObservableObject { @Published var pass: String = "" @Published var confirmPass: String = "" - let coordinator: ContentCoordinator + private let coordinator: ContentCoordinator init(coordinator: ContentCoordinator) { self.coordinator = coordinator From d19d0bdd16d0b30d409ffa58cbf30ef82abe9d4e Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Wed, 27 Nov 2024 19:10:23 +0300 Subject: [PATCH 10/13] Fix error --- Sources/FormView/FormView.swift | 37 ++++++++++++++++ .../FormView/Handler/FormStateHandler.swift | 44 ------------------- 2 files changed, 37 insertions(+), 44 deletions(-) delete mode 100644 Sources/FormView/Handler/FormStateHandler.swift diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 9e71033..2cca183 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -19,6 +19,43 @@ public enum ErrorHideBehaviour { case onFocusLost } +private class FormStateHandler: ObservableObject { + @Published var fieldStates: [FieldState] = .empty + @Published var currentFocusedFieldId: String = .empty + @Published var formValidator = FormValidator() + + func updateFieldStates(newStates: [FieldState]) { + fieldStates = newStates + + let focusedField = newStates.first { $0.isFocused } + currentFocusedFieldId = focusedField?.id ?? .empty + + // Замыкание onValidateRun вызывается методом validate() FormValidator'a. + formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in + guard let self else { + return false + } + + let resutls = newStates.map { $0.onValidate() } + + // Фокус на первом зафейленом филде. + if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { + currentFocusedFieldId = fieldStates[index].id + } + + return resutls.allSatisfy { $0 } + } + } + + func submit() { + currentFocusedFieldId = FocusService.getNextFocusFieldId( + states: fieldStates, + currentFocusField: currentFocusedFieldId + ) + } +} + + public struct FormView: View { @StateObject private var formStateHandler = FormStateHandler() @ViewBuilder private let content: (FormValidator) -> Content diff --git a/Sources/FormView/Handler/FormStateHandler.swift b/Sources/FormView/Handler/FormStateHandler.swift deleted file mode 100644 index 8f6b25d..0000000 --- a/Sources/FormView/Handler/FormStateHandler.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// FormStateHandler.swift -// -// -// Created by Victor Kostin on 27.11.2024. -// - -import SwiftUI - -class FormStateHandler: ObservableObject { - @Published var fieldStates: [FieldState] = .empty - @Published var currentFocusedFieldId: String = .empty - @Published var formValidator = FormValidator() - - func updateFieldStates(newStates: [FieldState]) { - fieldStates = newStates - - let focusedField = newStates.first { $0.isFocused } - currentFocusedFieldId = focusedField?.id ?? .empty - - // Замыкание onValidateRun вызывается методом validate() FormValidator'a. - formValidator.onValidateRun = { [weak self] focusOnFirstFailedField in - guard let self else { - return false - } - - let resutls = newStates.map { $0.onValidate() } - - // Фокус на первом зафейленом филде. - if let index = resutls.firstIndex(of: false), focusOnFirstFailedField { - currentFocusedFieldId = fieldStates[index].id - } - - return resutls.allSatisfy { $0 } - } - } - - func submit() { - currentFocusedFieldId = FocusService.getNextFocusFieldId( - states: fieldStates, - currentFocusField: currentFocusedFieldId - ) - } -} From 3e1c8e79ddd8c6a05d8045d0dc73531cf1cf9298 Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Fri, 29 Nov 2024 16:51:37 +0300 Subject: [PATCH 11/13] Fix mr --- ExampleApp/ExampleApp/Base/HostingController.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/ExampleApp/ExampleApp/Base/HostingController.swift b/ExampleApp/ExampleApp/Base/HostingController.swift index 14baef5..6c99025 100644 --- a/ExampleApp/ExampleApp/Base/HostingController.swift +++ b/ExampleApp/ExampleApp/Base/HostingController.swift @@ -9,8 +9,6 @@ import SwiftUI import UIKit class HostingController: UIHostingController { - var isNavigationBarHidden: Bool { false } - override init(rootView: T) { super.init(rootView: rootView) } @@ -23,7 +21,6 @@ class HostingController: UIHostingController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - } @available(*, unavailable) @MainActor dynamic required init?(coder aDecoder: NSCoder) { From 30665215dd9977b6ea922fd49718f08e6b46b9db Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Fri, 29 Nov 2024 16:51:45 +0300 Subject: [PATCH 12/13] Fix mr --- Sources/FormView/FormView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 2cca183..8323ccb 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -55,7 +55,6 @@ private class FormStateHandler: ObservableObject { } } - public struct FormView: View { @StateObject private var formStateHandler = FormStateHandler() @ViewBuilder private let content: (FormValidator) -> Content From 652cb84a12da9c421d2d7189fd6319826312808e Mon Sep 17 00:00:00 2001 From: Victor Kostin Date: Fri, 29 Nov 2024 16:53:44 +0300 Subject: [PATCH 13/13] Change class to struct --- Sources/FormView/FormView.swift | 2 +- Sources/FormView/Validation/Validators/FormValidator.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/FormView/FormView.swift b/Sources/FormView/FormView.swift index 8323ccb..035c708 100644 --- a/Sources/FormView/FormView.swift +++ b/Sources/FormView/FormView.swift @@ -22,7 +22,7 @@ public enum ErrorHideBehaviour { private class FormStateHandler: ObservableObject { @Published var fieldStates: [FieldState] = .empty @Published var currentFocusedFieldId: String = .empty - @Published var formValidator = FormValidator() + var formValidator = FormValidator() func updateFieldStates(newStates: [FieldState]) { fieldStates = newStates diff --git a/Sources/FormView/Validation/Validators/FormValidator.swift b/Sources/FormView/Validation/Validators/FormValidator.swift index c95d723..b493a70 100644 --- a/Sources/FormView/Validation/Validators/FormValidator.swift +++ b/Sources/FormView/Validation/Validators/FormValidator.swift @@ -7,7 +7,7 @@ import Foundation -public class FormValidator: ObservableObject { +public struct FormValidator { var onValidateRun: ((Bool) -> Bool)? public init() {