diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 815713e..0a28ff9 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 321F5CFD2ACA4D3100FB8AC5 /* PersonalScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321F5CFC2ACA4D3100FB8AC5 /* PersonalScreenView.swift */; }; 322788552AC35D0F00F16F06 /* ValidatableFormItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 322788542AC35D0F00F16F06 /* ValidatableFormItems.swift */; }; 322B458E2AC33BEB001917DE /* Forms in Frameworks */ = {isa = PBXBuildFile; productRef = 322B458D2AC33BEB001917DE /* Forms */; }; + 32BC643F2AE78924000A528B /* ReusableStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BC643E2AE78924000A528B /* ReusableStyles.swift */; }; + 32BC64412AE78F53000A528B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BC64402AE78F53000A528B /* Strings.swift */; }; 32EF92582ACA5934002278A7 /* PermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32EF92572ACA5934002278A7 /* PermissionsViewController.swift */; }; 32EF925A2ACA595C002278A7 /* PermissionsScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32EF92592ACA595C002278A7 /* PermissionsScreenView.swift */; }; 32EF925C2ACA5FD7002278A7 /* FinishedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32EF925B2ACA5FD7002278A7 /* FinishedViewController.swift */; }; @@ -37,6 +39,8 @@ 321F5CFA2ACA4D0A00FB8AC5 /* PersonalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalViewController.swift; sourceTree = ""; }; 321F5CFC2ACA4D3100FB8AC5 /* PersonalScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalScreenView.swift; sourceTree = ""; }; 322788542AC35D0F00F16F06 /* ValidatableFormItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatableFormItems.swift; sourceTree = ""; }; + 32BC643E2AE78924000A528B /* ReusableStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableStyles.swift; sourceTree = ""; }; + 32BC64402AE78F53000A528B /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 32EF92572ACA5934002278A7 /* PermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsViewController.swift; sourceTree = ""; }; 32EF92592ACA595C002278A7 /* PermissionsScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsScreenView.swift; sourceTree = ""; }; 32EF925B2ACA5FD7002278A7 /* FinishedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinishedViewController.swift; sourceTree = ""; }; @@ -83,6 +87,8 @@ 321225CC2AC33A5700DBE7E9 /* LaunchScreen.storyboard */, 321225CF2AC33A5700DBE7E9 /* Info.plist */, 322788542AC35D0F00F16F06 /* ValidatableFormItems.swift */, + 32BC643E2AE78924000A528B /* ReusableStyles.swift */, + 32BC64402AE78F53000A528B /* Strings.swift */, ); path = Example; sourceTree = ""; @@ -193,9 +199,11 @@ 321F5CFB2ACA4D0A00FB8AC5 /* PersonalViewController.swift in Sources */, 32EF925A2ACA595C002278A7 /* PermissionsScreenView.swift in Sources */, 321F5CF92ACA4ADC00FB8AC5 /* TermsScreenView.swift in Sources */, + 32BC64412AE78F53000A528B /* Strings.swift in Sources */, 322788552AC35D0F00F16F06 /* ValidatableFormItems.swift in Sources */, 321F5CF62ACA477A00FB8AC5 /* TermsViewController.swift in Sources */, 32EF925E2ACA6006002278A7 /* FinishedScreenView.swift in Sources */, + 32BC643F2AE78924000A528B /* ReusableStyles.swift in Sources */, 321225C42AC33A5600DBE7E9 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/Example/Controllers/PermissionsViewController.swift b/Example/Example/Controllers/PermissionsViewController.swift index facf1ee..0f2c369 100644 --- a/Example/Example/Controllers/PermissionsViewController.swift +++ b/Example/Example/Controllers/PermissionsViewController.swift @@ -41,28 +41,44 @@ final class PermissionsViewController: UIViewController { /// Subscribes to `.valueChangedPublisher` from custom extension /// located at `UIControl+Publishers.swift` screenView - .firstSwitchItem + .locationSwitchItem .isOnPublisher .receive(on: DispatchQueue.main) - .sink { print(">>> 1st FormSwitchItem \($0 ? "on" : "off")") } + .sink { [unowned self] in + print(">>> 1st FormSwitchItem \($0 ? "on" : "off")") + validateForm() + } .store(in: &cancellables) /// Subscribes to `.valueChangedPublisher` from custom extension /// located at `UIControl+Publishers.swift` screenView - .secondSwitchItem + .notificationsSwitchItem .isOnPublisher .receive(on: DispatchQueue.main) - .sink { print(">>> 2nd FormSwitchItem \($0 ? "on" : "off")") } + .sink { [unowned self] in + print(">>> 2nd FormSwitchItem \($0 ? "on" : "off")") + validateForm() + } .store(in: &cancellables) /// Subscribes to `.valueChangedPublisher` from custom extension /// located at `UIControl+Publishers.swift` screenView - .thirdSwitchItem + .cameraSwitchItem .isOnPublisher .receive(on: DispatchQueue.main) - .sink { print(">>> 3rd FormSwitchItem \($0 ? "on" : "off")") } + .sink { [unowned self] in + print(">>> 3rd FormSwitchItem \($0 ? "on" : "off")") + validateForm() + } .store(in: &cancellables) } + + // MARK: - Validation + + /// Validates the form and updates the UI accordingly. + func validateForm() { + screenView.buttonItem.isEnabled = screenView.formView.isValid + } } diff --git a/Example/Example/Controllers/PersonalViewController.swift b/Example/Example/Controllers/PersonalViewController.swift index 323c7f9..7a6b44f 100644 --- a/Example/Example/Controllers/PersonalViewController.swift +++ b/Example/Example/Controllers/PersonalViewController.swift @@ -29,21 +29,21 @@ final class PersonalViewController: UIViewController { /// Sets up target-action for buttons and other UI components. private func setupActions() { - screenView.inputItem.didChange = { [weak self] in + screenView.addressItem.didChange = { [weak self] in if let text = $0 { - print(">>> 'inputItem' = \(text)") + print(">>> 'addressItem' = \(text)") } self?.validateForm() } - screenView.numbersInputItem.didChange = { [weak self] in + screenView.phoneItem.didChange = { [weak self] in if let text = $0 { - print(">>> 'numbersInputItem' = \(text)") + print(">>> 'phoneItem' = \(text)") } self?.validateForm() } - screenView.requiredInputItem.didChange = { [weak self] in + screenView.nameItem.didChange = { [weak self] in if let text = $0 { - print(">>> 'requiredInputItem' = \(text)") + print(">>> 'nameItem' = \(text)") } self?.validateForm() } diff --git a/Example/Example/ReusableStyles.swift b/Example/Example/ReusableStyles.swift new file mode 100644 index 0000000..d40b2a3 --- /dev/null +++ b/Example/Example/ReusableStyles.swift @@ -0,0 +1,175 @@ +import Forms +import UIKit + +// MARK: - FormInputItem + +extension FormInputItem.Configuration { + static func with( + title: String, + placeholder: String + ) -> FormInputItem.Configuration { + FormInputItem.Configuration( + title: [ + NSAttributedString( + string: title, + attributes: [ + .font: UIFont(name: "AvenirNext-Medium", size: 16)!, + .foregroundColor: UIColor.label + ]) + ], + initialText: nil, + placeholder: [ + NSAttributedString( + string: placeholder, + attributes: [ + .foregroundColor: UIColor.tertiaryLabel, + .font: UIFont(name: "AvenirNext-Regular", size: 16)! + ]) + ], + isSecure: false, + autocorrectionType: .no, + autocapitalizationType: .none, + font: UIFont(name: "AvenirNext-Regular", size: 16)!, + textColor: UIColor.label, + cornerRadius: 8, + borderWidth: 1.0, + borderColor: UIColor.systemGray, + spacingAfter: 15, + didChange: nil + ) + } +} + + +// MARK: - FormSwitchItem + +extension FormSwitchItem.Configuration { + static func with( + title: String, + subtitle: String + ) -> FormSwitchItem.Configuration { + FormSwitchItem.Configuration( + title: [ + NSAttributedString( + string: title, + attributes: [ + .foregroundColor: UIColor.label, + .font: UIFont(name: "AvenirNext-Medium", size: 17)! + ]) + ], + subtitle: [ + NSAttributedString( + string: subtitle, + attributes: [ + .foregroundColor: UIColor.secondaryLabel, + .font: UIFont(name: "AvenirNext-Regular", size: 15)! + ]) + ], + onColor: .systemBlue, + isOn: false, + spacingAfter: 25 + ) + } +} + +// MARK: FormCheckboxItem + +extension FormCheckboxItem.Configuration { + static func with( + title: String, + subtitle: String, + highlightedSubtitle: String + ) -> FormCheckboxItem.Configuration { + FormCheckboxItem.Configuration( + title: [ + NSAttributedString( + string: title, + attributes: [ + .foregroundColor: UIColor.label, + .font: UIFont(name: "AvenirNext-Medium", size: 18)! + ]) + ], + subtitle: [ + NSAttributedString( + string: subtitle, + attributes: [ + .foregroundColor: UIColor.secondaryLabel, + .font: UIFont(name: "AvenirNext-Regular", size: 14)! + ]), + NSAttributedString( + string: highlightedSubtitle, + attributes: [ + .foregroundColor: UIColor.secondaryLabel, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .font: UIFont(name: "AvenirNext-Regular", size: 14)! + ]) + ], + checkedColor: UIColor.systemGreen, + uncheckedColor: UIColor.white, + borderWidth: 1.0, + borderColor: UIColor.lightGray, + cornerRadius: 5.0, + isChecked: false, + spacingAfter: 20.0, + shouldBeSelected: false + ) + } +} + +// MARK: FormTextItem + +extension FormTextItem.Configuration { + static func h1(_ title: String) -> FormTextItem.Configuration { + FormTextItem.Configuration( + text: [ + NSAttributedString( + string: title, + attributes: [ + .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, + .foregroundColor: UIColor.label, + .kern: 0.5 + ]) + ], + spacingAfter: 20 + ) + } + + static func h2(_ title: String) -> FormTextItem.Configuration { + FormTextItem.Configuration( + text: [ + NSAttributedString( + string: title, + attributes: [ + .font: UIFont(name: "AvenirNext-Regular", size: 16)!, + .foregroundColor: UIColor.secondaryLabel, + .kern: 0.2 + ]) + ], + spacingAfter: 20 + ) + } +} + +// MARK: FormButtonItem + +extension FormButtonItem.Configuration { + static func withTitle(_ title: String) -> FormButtonItem.Configuration { + FormButtonItem.Configuration( + title: [ + NSAttributedString( + string: title, + attributes: [ + .font: UIFont(name: "AvenirNext-Bold", size: 20)!, + .foregroundColor: UIColor.white + ]) + ], + enabledColor: UIColor.systemBlue, + disabledColor: UIColor.systemGray, + borderWidth: 1.0, + borderColor: UIColor.clear, + cornerRadius: 10.0, + spacingAfter: 0, + shouldBeEnabled: false + ) + } +} diff --git a/Example/Example/Strings.swift b/Example/Example/Strings.swift new file mode 100644 index 0000000..1a7c945 --- /dev/null +++ b/Example/Example/Strings.swift @@ -0,0 +1,40 @@ +enum Strings { + enum Personal { + static let title = "Your personal details" + static let subtitle = "Insert your personal information to keep your profile up to date." + static let addressTitle = "Street Address" + static let addressPlaceholder = "e.g. 5912 5th Avenue, New York, NY" + static let nameTitle = "Full Name" + static let namePlaceholder = "Required" + static let phoneTitle = "Phone Number" + static let phonePlaceholder = "Only numbers allowed" + static let calendarTitle = "Date of Birth" + static let button = "Continue to Permissions" + } + + enum Permissions { + static let title = "Manage Permissions" + static let subtitle = "Customize which features the app can access to enhance your user experience." + static let locationTitle = "Location Access" + static let locationSubtitle = "Allow the app to access your location to enhance service delivery and improve user experience." + static let notificationsTitle = "Notifications" + static let notificationsSubtitleA = "Enable notifications to stay updated with the latest news, updates, and offers.\n" + static let notificationsSubtitleB = "Enabling this permission is required." + static let cameraTitle = "Camera Access" + static let cameraSubtitle = "Grant permission to access your camera to take photos and videos within the app." + static let button = "Continue to T&C" + } + + enum Terms { + static let title = "Review Our Terms & Conditions" + static let subtitle = "Please read carefully to understand your rights and obligations while using our services." + static let checkboxTitle = "Acceptance of Terms & Conditions" + static let checkboxSubtitle = "By checking this box, " + static let checkboxSubtitleHighlighted = "you acknowledge that you have read, understood, and agree to abide by the terms and conditions outlined above." + static let button = "Continue to Registration" + } + + enum Finished { + static let title = "Horray! You finished the onboarding. 🎉" + } +} diff --git a/Example/Example/ValidatableFormItems.swift b/Example/Example/ValidatableFormItems.swift index c30b8da..1f9fc8d 100644 --- a/Example/Example/ValidatableFormItems.swift +++ b/Example/Example/ValidatableFormItems.swift @@ -36,6 +36,15 @@ final class RegexFormInputItem: FormInputItem, Validatable { /// `RequiredFormCheckboxItem` is a subclass of `FormCheckboxItem` and conforms to `Validatable`. /// It considers the item valid if it is selected. final class RequiredFormCheckboxItem: FormCheckboxItem, Validatable { - /// A computed property to check if the checkbox item is selected and therefore valid. - var isValid: Bool { isSelected } + + /// A computed property to check if the value is valid. + var isValid: Bool { value == true } +} + +/// `RequiredFormSwitchItem` is a subclass of `FormSwitchItem` and conforms to `Validatable`. +/// It considers the item valid if it is enabled. +final class RequiredFormSwitchItem: FormSwitchItem, Validatable { + + /// A computed property to check if the value is valid. + var isValid: Bool { value == true } } diff --git a/Example/Example/Views/FinishedScreenView.swift b/Example/Example/Views/FinishedScreenView.swift index 1c25057..90a2108 100644 --- a/Example/Example/Views/FinishedScreenView.swift +++ b/Example/Example/Views/FinishedScreenView.swift @@ -8,7 +8,7 @@ final class FinishedScreenView: UIView { // MARK: - Properties let formView = FormView(elements: [ - FormTextItem(configuration: .title) + FormTextItem(configuration: .h1(Strings.Finished.title)) ]) init() { @@ -28,17 +28,3 @@ final class FinishedScreenView: UIView { required init?(coder: NSCoder) { nil } } - -// MARK: - FormItem.Configuration - -private extension FormTextItem.Configuration { - static let title = FormTextItem.Configuration( - text: "Horray! You finished the onboarding. 🎉", - attributes: [ - .font: UIFont(name: "AvenirNext-DemiBold", size: 30)!, - .foregroundColor: UIColor.label, - .kern: 0.5, - ], - spacingAfter: 20 - ) -} diff --git a/Example/Example/Views/PermissionsScreenView.swift b/Example/Example/Views/PermissionsScreenView.swift index d95f24d..6575e23 100644 --- a/Example/Example/Views/PermissionsScreenView.swift +++ b/Example/Example/Views/PermissionsScreenView.swift @@ -8,19 +8,53 @@ final class PermissionsScreenView: UIView { // MARK: - Properties lazy var formView = FormView(elements: [ - FormTextItem(configuration: .permissionsTitle), - FormTextItem(configuration: .permissionsSubtitle), - firstSwitchItem, - secondSwitchItem, - thirdSwitchItem, + FormTextItem(configuration: .h1(Strings.Permissions.title)), + FormTextItem(configuration: .h2(Strings.Permissions.subtitle)), + locationSwitchItem, + notificationsSwitchItem, + cameraSwitchItem, FormSpacingItem(), buttonItem ]) - let firstSwitchItem = FormSwitchItem(configuration: .first) - let secondSwitchItem = FormSwitchItem(configuration: .second) - let thirdSwitchItem = FormSwitchItem(configuration: .third) - let buttonItem = FormButtonItem(configuration: .terms) + let locationSwitchItem = FormSwitchItem(configuration: .with( + title: Strings.Permissions.locationTitle, + subtitle: Strings.Permissions.locationSubtitle + )) + let notificationsSwitchItem = RequiredFormSwitchItem(configuration: .init( + title: [ + NSAttributedString( + string: Strings.Permissions.notificationsTitle, + attributes: [ + .foregroundColor: UIColor.label, + .font: UIFont(name: "AvenirNext-Medium", size: 17)! + ]) + ], + subtitle: [ + NSAttributedString( + string: Strings.Permissions.notificationsSubtitleA, + attributes: [ + .foregroundColor: UIColor.secondaryLabel, + .font: UIFont(name: "AvenirNext-Regular", size: 15)! + ]), + NSAttributedString( + string: Strings.Permissions.notificationsSubtitleB, + attributes: [ + .foregroundColor: UIColor.secondaryLabel, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: UIColor.secondaryLabel, + .font: UIFont(name: "AvenirNext-Medium", size: 14)! + ]) + ], + onColor: .systemGreen, + isOn: false, + spacingAfter: 25 + )) + let cameraSwitchItem = FormSwitchItem(configuration: .with( + title: Strings.Permissions.cameraTitle, + subtitle: Strings.Permissions.cameraSubtitle + )) + let buttonItem = FormButtonItem(configuration: .withTitle(Strings.Permissions.button)) init() { super.init(frame: .zero) @@ -39,82 +73,3 @@ final class PermissionsScreenView: UIView { required init?(coder: NSCoder) { nil } } - -// MARK: - FormItem.Configuration - -private extension FormTextItem.Configuration { - static let permissionsTitle = FormTextItem.Configuration( - text: "Manage Permissions", - attributes: [ - .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.label, - .kern: 0.5 - ], - spacingAfter: 20 - ) - - static let permissionsSubtitle = FormTextItem.Configuration( - text: "Customize which features the app can access to enhance your user experience.", - attributes: [ - .font: UIFont(name: "AvenirNext-Regular", size: 16)!, - .foregroundColor: UIColor.secondaryLabel, - .kern: 0.2 - ], - spacingAfter: 20 - ) -} - -private extension FormSwitchItem.Configuration { - static let first = FormSwitchItem.Configuration( - title: "Location Access", - titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .label, - subtitle: "Allow the app to access your location to enhance service delivery and improve user experience.", - subtitleFont: UIFont(name: "AvenirNext-Regular", size: 15)!, - subtitleColor: .secondaryLabel, - onColor: .systemBlue, - isOn: false, - spacingAfter: 25 - ) - - static let second = FormSwitchItem.Configuration( - title: "Notifications", - titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .label, - subtitle: "Enable notifications to stay updated with the latest news, updates, and offers.", - subtitleFont: UIFont(name: "AvenirNext-Regular", size: 15)!, - subtitleColor: .secondaryLabel, - onColor: .systemGreen, - isOn: false, - spacingAfter: 25 - ) - - static let third = FormSwitchItem.Configuration( - title: "Camera Access", - titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .label, - subtitle: "Grant permission to access your camera to take photos and videos within the app.", - subtitleFont: UIFont(name: "AvenirNext-Regular", size: 15)!, - subtitleColor: .secondaryLabel, - onColor: .systemRed, - isOn: false, - spacingAfter: 25 - ) -} - -private extension FormButtonItem.Configuration { - static let terms = FormButtonItem.Configuration( - title: "Continue to T&C", - attributes: [ - .font: UIFont(name: "AvenirNext-Bold", size: 20)!, - .foregroundColor: UIColor.white - ], - enabledColor: UIColor.systemBlue, - disabledColor: UIColor.systemGray, - borderWidth: 1.0, - borderColor: UIColor.clear, - cornerRadius: 10.0, - spacingAfter: 0, - shouldBeEnabled: true - ) -} diff --git a/Example/Example/Views/PersonalScreenView.swift b/Example/Example/Views/PersonalScreenView.swift index 3190597..a012211 100644 --- a/Example/Example/Views/PersonalScreenView.swift +++ b/Example/Example/Views/PersonalScreenView.swift @@ -13,22 +13,35 @@ final class PersonalScreenView: UIView { // MARK: - Properties lazy var formView = FormView(elements: [ - FormTextItem(configuration: .title), - FormTextItem(configuration: .subtitle), - requiredInputItem, - numbersInputItem, - inputItem, + FormTextItem(configuration: .h1(Strings.Personal.title)), + FormTextItem(configuration: .h2(Strings.Personal.subtitle)), + nameItem, + phoneItem, + addressItem, calendarItem, FormSpacingItem(), buttonItem ]) let scrollView = UIScrollView() - let inputItem = FormInputItem(configuration: .first) - let requiredInputItem = MinimumFormInputItem(configuration: .second) - let numbersInputItem = RegexFormInputItem(configuration: .third) - let buttonItem = FormButtonItem(configuration: .personal) - lazy var calendarItem = FormCalendarItem(configuration: .personal(delegate: self)) + let addressItem = FormInputItem(configuration: .with( + title: Strings.Personal.addressTitle, + placeholder: Strings.Personal.addressPlaceholder + )) + let nameItem = MinimumFormInputItem(configuration: .with( + title: Strings.Personal.nameTitle, + placeholder: Strings.Personal.namePlaceholder + )) + let phoneItem = RegexFormInputItem(configuration: .with( + title: Strings.Personal.phoneTitle, + placeholder: Strings.Personal.phonePlaceholder + )) + lazy var calendarItem = FormCalendarItem( + configuration: .personal(delegate: self) + ) + let buttonItem = FormButtonItem( + configuration: .withTitle(Strings.Personal.button) + ) init() { super.init(frame: .zero) @@ -117,126 +130,26 @@ extension PersonalScreenView: UICalendarViewDelegate { } } -// MARK: - FormItem.Configuration - -private extension FormTextItem.Configuration { - static let title = FormTextItem.Configuration( - text: "Your personal details", - attributes: [ - .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.label, - .kern: 0.5 - ], - spacingAfter: 20 - ) - - static let subtitle = FormTextItem.Configuration( - text: "Insert your personal information to keep your profile up to date.", - attributes: [ - .font: UIFont(name: "AvenirNext-Regular", size: 16)!, - .foregroundColor: UIColor.secondaryLabel, - .kern: 0.2 - ], - spacingAfter: 20 - ) -} - -private extension FormInputItem.Configuration { - static let first = FormInputItem.Configuration( - title: "Street Address", - titleAttributes: [ - .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.label - ], - initialText: nil, - placeholder: "e.g. 5912 5th Avenue, New York, NY", - isSecure: false, - autocorrectionType: .no, - autocapitalizationType: .none, - font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.label, - cornerRadius: 8, - borderWidth: 1.0, - borderColor: UIColor.systemGray, - spacingAfter: 15, - didChange: nil - ) - - static let second = FormInputItem.Configuration( - title: "Full Name", - titleAttributes: [ - .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.label - ], - initialText: nil, - placeholder: "Required", - isSecure: false, - autocorrectionType: .default, - autocapitalizationType: .words, - font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.label, - cornerRadius: 8, - borderWidth: 1.0, - borderColor: UIColor.lightGray, - spacingAfter: 15, - didChange: nil - ) - - static let third = FormInputItem.Configuration( - title: "Phone Number", - titleAttributes: [ - .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.label - ], - initialText: nil, - placeholder: "Only numbers allowed", - isSecure: false, - autocorrectionType: .no, - autocapitalizationType: .none, - font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.label, - cornerRadius: 8, - borderWidth: 1.0, - borderColor: UIColor.lightGray, - spacingAfter: 15, - didChange: nil - ) -} - -private extension FormButtonItem.Configuration { - static let personal = FormButtonItem.Configuration( - title: "Continue to Permissions", - attributes: [ - .font: UIFont(name: "AvenirNext-DemiBold", size: 18)!, - .foregroundColor: UIColor.white - ], - enabledColor: .systemBlue, - disabledColor: .systemGray3, - borderWidth: 1.5, - borderColor: .systemBlue, - cornerRadius: 20.0, - spacingAfter: 20, - shouldBeEnabled: false - ) -} - private extension FormCalendarItem.Configuration { static func personal( delegate: CalendarDelegate ) -> FormCalendarItem.Configuration { FormCalendarItem.Configuration( - title: "Date of Birth", + title: [ + NSAttributedString( + string: Strings.Personal.calendarTitle, + attributes: [ + .font: UIFont(name: "AvenirNext-Medium", size: 16)!, + .foregroundColor: UIColor.label + ]) + ], calendar: .init(identifier: .gregorian), tintColor: .label, spacingAfter: 20, availableRange: DateInterval(start: .distantPast, end: .now), delegate: delegate, selectionMultiDate: nil, - selectionSingleDate: .selectionSingleDate(delegate: delegate), - titleAttributes: [ - .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.label - ] + selectionSingleDate: .selectionSingleDate(delegate: delegate) ) } } diff --git a/Example/Example/Views/TermsScreenView.swift b/Example/Example/Views/TermsScreenView.swift index 1d5586f..48f7200 100644 --- a/Example/Example/Views/TermsScreenView.swift +++ b/Example/Example/Views/TermsScreenView.swift @@ -8,15 +8,23 @@ final class TermsScreenView: UIView { // MARK: - Properties lazy var formView = FormView(elements: [ - FormTextItem(configuration: .termsTitle), - FormTextItem(configuration: .termsSubtitle), + FormTextItem(configuration: .h1(Strings.Terms.title)), + FormTextItem(configuration: .h2(Strings.Terms.subtitle)), FormSpacingItem(), checkboxItem, buttonItem ]) - let buttonItem = FormButtonItem(configuration: .terms) - let checkboxItem = RequiredFormCheckboxItem(configuration: .terms) + let checkboxItem = RequiredFormCheckboxItem( + configuration: .with( + title: Strings.Terms.checkboxTitle, + subtitle: Strings.Terms.checkboxSubtitle, + highlightedSubtitle: Strings.Terms.checkboxSubtitleHighlighted + ) + ) + let buttonItem = FormButtonItem( + configuration: .withTitle(Strings.Terms.button) + ) init() { super.init(frame: .zero) @@ -35,63 +43,3 @@ final class TermsScreenView: UIView { required init?(coder: NSCoder) { nil } } - -// MARK: - FormItem.Configuration - -private extension FormTextItem.Configuration { - static let termsTitle = FormTextItem.Configuration( - text: "Review Our Terms & Conditions", - attributes: [ - .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.label, - .kern: 0.5 - ], - spacingAfter: 20 - ) - - static let termsSubtitle = FormTextItem.Configuration( - text: "Please read carefully to understand your rights and obligations while using our services.", - attributes: [ - .font: UIFont(name: "AvenirNext-Regular", size: 16)!, - .foregroundColor: UIColor.secondaryLabel, - .kern: 0.2 - ], - spacingAfter: 20 - ) -} - -private extension FormCheckboxItem.Configuration { - static let terms = FormCheckboxItem.Configuration( - title: "Acceptance of Terms & Conditions", - titleFont: UIFont(name: "AvenirNext-Medium", size: 18)!, - titleColor: UIColor.label, - subtitle: "By checking this box, you acknowledge that you have read, understood, and agree to abide by the terms and conditions outlined above.", - subtitleFont: UIFont(name: "AvenirNext-Regular", size: 14)!, - subtitleColor: UIColor.secondaryLabel, - checkedColor: UIColor.systemGreen, - uncheckedColor: UIColor.white, - borderWidth: 1.0, - borderColor: UIColor.lightGray, - cornerRadius: 5.0, - isChecked: false, - spacingAfter: 20.0, - shouldBeSelected: false - ) -} - -private extension FormButtonItem.Configuration { - static let terms = FormButtonItem.Configuration( - title: "Continue to Registration", - attributes: [ - .font: UIFont(name: "AvenirNext-Bold", size: 20)!, - .foregroundColor: UIColor.white - ], - enabledColor: UIColor.systemBlue, - disabledColor: UIColor.systemGray, - borderWidth: 1.0, - borderColor: UIColor.clear, - cornerRadius: 10.0, - spacingAfter: 0, - shouldBeEnabled: false - ) -} diff --git a/Sources/Forms/FormButtonItem.swift b/Sources/Forms/FormButtonItem.swift index b980230..47ef6b1 100644 --- a/Sources/Forms/FormButtonItem.swift +++ b/Sources/Forms/FormButtonItem.swift @@ -15,8 +15,7 @@ open class FormButtonItem: UIControl, FormItem { /// It holds all the customizable parameters, which include visual attributes /// and spacing information for the button in the form. public struct Configuration { - let title: String - let attributes: [NSAttributedString.Key: Any] + let title: [NSAttributedString] let enabledColor: UIColor let disabledColor: UIColor let borderWidth: CGFloat @@ -27,7 +26,7 @@ open class FormButtonItem: UIControl, FormItem { /// Initializes a new instance of `FormButtonItem.Configuration`. /// - Parameters: - /// - title: The title of the button item. + /// - title: A collection of attributed strings that will compose the title of the button item. /// - attributes: A dictionary with the attributes for the title label. /// - enabledColor: The background color when the button is enabled. /// - disabledColor: The background color when the button is disabled. @@ -37,8 +36,7 @@ open class FormButtonItem: UIControl, FormItem { /// - spacingAfter: The space after the button item in the form. /// - shouldBeEnabled: The initial state of the button item. public init( - title: String, - attributes: [NSAttributedString.Key : Any], + title: [NSAttributedString], enabledColor: UIColor, disabledColor: UIColor, borderWidth: CGFloat, @@ -48,7 +46,6 @@ open class FormButtonItem: UIControl, FormItem { shouldBeEnabled: Bool ) { self.title = title - self.attributes = attributes self.enabledColor = enabledColor self.disabledColor = disabledColor self.borderWidth = borderWidth @@ -92,10 +89,9 @@ open class FormButtonItem: UIControl, FormItem { /// - configuration: The model containing all the attributes of the button item. public init(configuration: Configuration) { titleLabel.numberOfLines = 0 - titleLabel.attributedText = NSAttributedString( - string: configuration.title, - attributes: configuration.attributes - ) + titleLabel.attributedText = configuration.title.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } spacingAfter = configuration.spacingAfter enabledColor = configuration.enabledColor disabledColor = configuration.disabledColor diff --git a/Sources/Forms/FormCalendarItem.swift b/Sources/Forms/FormCalendarItem.swift index 3792630..887abb6 100644 --- a/Sources/Forms/FormCalendarItem.swift +++ b/Sources/Forms/FormCalendarItem.swift @@ -49,7 +49,7 @@ open class FormCalendarItem: UIView, FormItem { } } - let title: String + let title: [NSAttributedString] let calendar: Calendar let delegate: UICalendarViewDelegate? let tintColor: UIColor @@ -57,11 +57,10 @@ open class FormCalendarItem: UIView, FormItem { let availableRange: DateInterval? let selectionMultiDate: SelectionMultiDate? let selectionSingleDate: SelectionSingleDate? - let titleAttributes: [NSAttributedString.Key: Any] /// Initializes a new instance of `FormCalendarItem.Configuration`. /// - Parameters: - /// - title: The title of the calendar item. + /// - title: A collection of attributed strings that will compose the title of the calendar item. /// - calendar: The calendar that the calendar item illustrates. /// - tintColor: The tint color of the calendar view. /// - spacingAfter: The space after the calendar item in the form. @@ -69,17 +68,15 @@ open class FormCalendarItem: UIView, FormItem { /// - delegate: The object that acts as the delegate of the calendar view. /// - selectionMultiDate: Configuration for the selection of multiple dates. /// - selectionSingleDate: Configuration for the selection of a single date. - /// - titleAttributes: A dictionary with the attributes for the title label. public init( - title: String, + title: [NSAttributedString], calendar: Calendar, tintColor: UIColor, spacingAfter: CGFloat, availableRange: DateInterval?, delegate: UICalendarViewDelegate?, selectionMultiDate: SelectionMultiDate?, - selectionSingleDate: SelectionSingleDate?, - titleAttributes: [NSAttributedString.Key : Any] + selectionSingleDate: SelectionSingleDate? ) { self.title = title self.calendar = calendar @@ -87,15 +84,14 @@ open class FormCalendarItem: UIView, FormItem { self.tintColor = tintColor self.spacingAfter = spacingAfter self.availableRange = availableRange - self.titleAttributes = titleAttributes self.selectionMultiDate = selectionMultiDate self.selectionSingleDate = selectionSingleDate } } - private let titleLabel = UILabel() private let stackView = UIStackView() - private let calendarView = UICalendarView() + private(set) var titleLabel = UILabel() + private(set) var calendarView = UICalendarView() /// The space after the calendar item in the form. public var spacingAfter: CGFloat @@ -107,10 +103,9 @@ open class FormCalendarItem: UIView, FormItem { calendarView.tintColor = configuration.tintColor titleLabel.numberOfLines = 0 - titleLabel.attributedText = NSAttributedString( - string: configuration.title, - attributes: configuration.titleAttributes - ) + titleLabel.attributedText = configuration.title.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } if let availableRange = configuration.availableRange { calendarView.availableDateRange = availableRange @@ -132,9 +127,7 @@ open class FormCalendarItem: UIView, FormItem { setupViews() } - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + required public init?(coder: NSCoder) { nil } private func setupViews() { addSubview(stackView) diff --git a/Sources/Forms/FormCheckboxItem.swift b/Sources/Forms/FormCheckboxItem.swift index d1ce9cf..2ac4e03 100644 --- a/Sources/Forms/FormCheckboxItem.swift +++ b/Sources/Forms/FormCheckboxItem.swift @@ -8,19 +8,15 @@ import UIKit /// provides extensive customization options. /// /// - Note: This class conforms to the `FormItem` protocol. -open class FormCheckboxItem: UIView, FormItem { +open class FormCheckboxItem: UIView, FormInputType { /// A structure used to configure a `FormCheckboxItem`. /// /// It holds all the customizable parameters, which include visual attributes /// and spacing information for the checkbox item in the form. public struct Configuration { - let title: String - let titleFont: UIFont - let titleColor: UIColor - let subtitle: String? - let subtitleFont: UIFont - let subtitleColor: UIColor + let title: [NSAttributedString] + let subtitle: [NSAttributedString]? let checkedColor: UIColor let uncheckedColor: UIColor let borderWidth: CGFloat @@ -32,12 +28,8 @@ open class FormCheckboxItem: UIView, FormItem { /// Initializes a new instance of `FormCheckboxItem.Configuration`. /// - Parameters: - /// - title: The title of the checkbox item. - /// - titleFont: The font of the title label. - /// - titleColor: The text color of the title label. - /// - subtitle: The subtitle of the checkbox item. - /// - subtitleFont: The font of the subtitle label. - /// - subtitleColor: The text color of the subtitle label. + /// - title: A collection of attributed strings that will compose the title of the checkbox item. + /// - subtitle: A collection of attributed strings that will compose the subtitle of the checkbox item. /// - checkedColor: The background color when the checkbox is selected. /// - uncheckedColor: The background color when the checkbox is not selected. /// - borderWidth: The width of the border of the checkbox. @@ -47,12 +39,8 @@ open class FormCheckboxItem: UIView, FormItem { /// - spacingAfter: The space after the checkbox item in the form. /// - shouldBeSelected: The initial state of the component item. public init( - title: String, - titleFont: UIFont, - titleColor: UIColor, - subtitle: String?, - subtitleFont: UIFont, - subtitleColor: UIColor, + title: [NSAttributedString], + subtitle: [NSAttributedString]?, checkedColor: UIColor, uncheckedColor: UIColor, borderWidth: CGFloat, @@ -63,11 +51,7 @@ open class FormCheckboxItem: UIView, FormItem { shouldBeSelected: Bool ) { self.title = title - self.titleFont = titleFont - self.titleColor = titleColor self.subtitle = subtitle - self.subtitleFont = subtitleFont - self.subtitleColor = subtitleColor self.checkedColor = checkedColor self.uncheckedColor = uncheckedColor self.borderWidth = borderWidth @@ -111,6 +95,11 @@ open class FormCheckboxItem: UIView, FormItem { } } + /// The current value in the checkbox. + public var value: Bool { + isSelected + } + /// A closure that is invoked when the checkbox is tapped. public var didSelect: (() -> Void)? @@ -118,14 +107,16 @@ open class FormCheckboxItem: UIView, FormItem { /// - Parameters: /// - configuration: The model containing all the attributes of the checkbox item. public init(configuration: Configuration) { - titleLabel.text = configuration.title - titleLabel.font = configuration.titleFont titleLabel.numberOfLines = 0 - titleLabel.textColor = configuration.titleColor - subtitleLabel.text = configuration.subtitle subtitleLabel.numberOfLines = 0 - subtitleLabel.font = configuration.subtitleFont - subtitleLabel.textColor = configuration.subtitleColor + titleLabel.attributedText = configuration.title.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + if let subtitle = configuration.subtitle { + subtitleLabel.attributedText = subtitle.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + } filledColor = configuration.checkedColor emptyColor = configuration.uncheckedColor controlView.isSelected = configuration.isChecked diff --git a/Sources/Forms/FormInputItem.swift b/Sources/Forms/FormInputItem.swift index 8708040..d76db7d 100644 --- a/Sources/Forms/FormInputItem.swift +++ b/Sources/Forms/FormInputItem.swift @@ -15,10 +15,9 @@ open class FormInputItem: UIView, FormInputType { /// It holds all the customizable parameters, which include visual attributes /// and spacing information for the input item in the form. public struct Configuration { - let title: String? - let titleAttributes: [NSAttributedString.Key: Any] + let title: [NSAttributedString] + let placeholder: [NSAttributedString] let initialText: String? - let placeholder: String? let isSecure: Bool let autocorrectionType: UITextAutocorrectionType let autocapitalizationType: UITextAutocapitalizationType @@ -32,10 +31,9 @@ open class FormInputItem: UIView, FormInputType { /// Initializes a new instance of `FormInputItem.Configuration`. /// - Parameters: - /// - title: The title of the input item. - /// - attributes: A dictionary with the attributes for the title label. + /// - title: A collection of attributed strings that will compose the title of the input item. /// - initialText: The initial text in the text field. - /// - placeholder: The placeholder text in the text field. + /// - placeholder: A collection of attributed strings that will compose the placeholder of the input item. /// - font: The font of the text field. /// - textColor: The text color of the text field. /// - cornerRadius: The corner radius of the container view. @@ -43,10 +41,9 @@ open class FormInputItem: UIView, FormInputType { /// - borderColor: The color of the border of the container view. /// - spacingAfter: The space after the input item in the form. public init( - title: String?, - titleAttributes: [NSAttributedString.Key: Any], + title: [NSAttributedString], initialText: String?, - placeholder: String?, + placeholder: [NSAttributedString], isSecure: Bool, autocorrectionType: UITextAutocorrectionType, autocapitalizationType: UITextAutocapitalizationType, @@ -59,7 +56,6 @@ open class FormInputItem: UIView, FormInputType { didChange: ((String?) -> Void)? ) { self.title = title - self.titleAttributes = titleAttributes self.initialText = initialText self.placeholder = placeholder self.isSecure = isSecure @@ -101,14 +97,17 @@ open class FormInputItem: UIView, FormInputType { /// - configuration: The model containing all the attributes of the input item. public init(configuration: Configuration) { titleLabel.numberOfLines = 0 - titleLabel.attributedText = NSAttributedString( - string: configuration.title ?? "", - attributes: configuration.titleAttributes - ) + titleLabel.attributedText = configuration.title.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + textField.font = configuration.font textField.text = configuration.initialText textField.textColor = configuration.textColor - textField.placeholder = configuration.placeholder + textField.attributedPlaceholder = configuration.placeholder.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + textField.isSecureTextEntry = configuration.isSecure textField.autocorrectionType = configuration.autocorrectionType textField.autocapitalizationType = configuration.autocapitalizationType diff --git a/Sources/Forms/FormSwitchItem.swift b/Sources/Forms/FormSwitchItem.swift index 1ed7b08..dd888ed 100644 --- a/Sources/Forms/FormSwitchItem.swift +++ b/Sources/Forms/FormSwitchItem.swift @@ -9,53 +9,37 @@ import UIKit /// and color schemes, enabling the creation of visually cohesive and user-friendly form items. /// /// - Note: This class conforms to the `FormItem` protocol. -open class FormSwitchItem: UIView, FormItem { +open class FormSwitchItem: UIView, FormInputType { /// A structure used to configure a `FormSwitchItem`. /// /// It holds all the customizable parameters, which include visual attributes /// and spacing information for the switch item in the form. public struct Configuration { - let title: String - let titleFont: UIFont - let titleColor: UIColor - let subtitle: String? - let subtitleFont: UIFont - let subtitleColor: UIColor - let onColor: UIColor + let title: [NSAttributedString] + let subtitle: [NSAttributedString]? let isOn: Bool + let onColor: UIColor let spacingAfter: CGFloat /// Initializes a new instance of `FormSwitchItem.Configuration`. /// - Parameters: - /// - title: The title of the switcher item. - /// - titleFont: The font of the title label. - /// - titleColor: The text color of the title label. - /// - subtitle: The subtitle of the switcher item. - /// - subtitleFont: The font of the subtitle label. - /// - subtitleColor: The text color of the subtitle label. - /// - onColor: The tint color when the switch is toggled on. + /// - title: A collection of attributed strings that will compose the title of the switcher item. + /// - subtitle: A collection of attributed strings that will compose the subtitle of the switcher item. /// - isOn: The initial state of the switch item. + /// - onColor: The tint color when the switch is toggled on. /// - spacingAfter: The space after the switch item in the form. public init( - title: String, - titleFont: UIFont, - titleColor: UIColor, - subtitle: String?, - subtitleFont: UIFont, - subtitleColor: UIColor, + title: [NSAttributedString], + subtitle: [NSAttributedString]?, onColor: UIColor, isOn: Bool, spacingAfter: CGFloat ) { self.title = title - self.titleFont = titleFont - self.titleColor = titleColor self.subtitle = subtitle - self.subtitleFont = subtitleFont - self.subtitleColor = subtitleColor - self.onColor = onColor self.isOn = isOn + self.onColor = onColor self.spacingAfter = spacingAfter } } @@ -81,18 +65,25 @@ open class FormSwitchItem: UIView, FormItem { /// A closure that is invoked when the switch is toggled. public var didToggle: ((Bool) -> Void)? + /// The current value in the switcher. + public var value: Bool { + switchView.isOn + } + /// Initializes a new instance of `FormSwitchItem`. /// - Parameters: /// - configuration: The model containing all the attributes of the switch item. public init(configuration: Configuration) { - titleLabel.text = configuration.title titleLabel.numberOfLines = 0 subtitleLabel.numberOfLines = 0 - titleLabel.font = configuration.titleFont - titleLabel.textColor = configuration.titleColor - subtitleLabel.text = configuration.subtitle - subtitleLabel.font = configuration.subtitleFont - subtitleLabel.textColor = configuration.subtitleColor + titleLabel.attributedText = configuration.title.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + if let subtitle = configuration.subtitle { + subtitleLabel.attributedText = subtitle.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } + } onColor = configuration.onColor switchView.isOn = configuration.isOn switchView.onTintColor = configuration.onColor @@ -146,7 +137,11 @@ open class FormSwitchItem: UIView, FormItem { titleLabel.accessibilityLabel = titleLabel.text titleLabel.accessibilityTraits = .header switchView.accessibilityLabel = "Switch" - switchView.accessibilityTraits = .button // N.B: `.toggleButton` is only available on iOS 17 + if #available(iOS 17.0, *) { + switchView.accessibilityTraits = .toggleButton + } else { + switchView.accessibilityTraits = .button + } } @objc private func didToggleSwitch() { diff --git a/Sources/Forms/FormTextItem.swift b/Sources/Forms/FormTextItem.swift index 0360cdd..bb73d53 100644 --- a/Sources/Forms/FormTextItem.swift +++ b/Sources/Forms/FormTextItem.swift @@ -14,22 +14,18 @@ open class FormTextItem: UIView, FormItem { /// It holds all the customizable parameters, which include visual attributes /// and spacing information for the text item in the form. public struct Configuration { - let text: String - let attributes: [NSAttributedString.Key: Any] + let text: [NSAttributedString] let spacingAfter: CGFloat - /// Initializes a new instance of `FormCheckboxItem.Configuration`. + /// Initializes a new instance of `FormTextItem.Configuration`. /// - Parameters: - /// - title: The title of the text item. - /// - attributes: A dictionary with the attributes for the title label. + /// - text: A collection of attributed strings that will compose the item. /// - spacingAfter: The space after the text item in the form. public init( - text: String, - attributes: [NSAttributedString.Key : Any], + text: [NSAttributedString], spacingAfter: CGFloat ) { self.text = text - self.attributes = attributes self.spacingAfter = spacingAfter } } @@ -41,15 +37,12 @@ open class FormTextItem: UIView, FormItem { /// Initializes a new instance of `FormTextItem` with the provided text attributes. /// - Parameters: - /// - text: The text of the item. - /// - attributes: The attributes to apply to the text. - /// - spacingAfter: The space after the text item in the form. + /// - configuration: The model containing all the attributes of the text item. public init(configuration: Configuration) { textLabel.numberOfLines = 0 - textLabel.attributedText = NSAttributedString( - string: configuration.text, - attributes: configuration.attributes - ) + textLabel.attributedText = configuration.text.reduce( + into: NSMutableAttributedString() + ) { $0.append($1) } spacingAfter = configuration.spacingAfter super.init(frame: .zero) diff --git a/Sources/Forms/FormView.swift b/Sources/Forms/FormView.swift index 9364aa6..7118e16 100644 --- a/Sources/Forms/FormView.swift +++ b/Sources/Forms/FormView.swift @@ -10,7 +10,9 @@ open class FormView: UIView { /// A Boolean value indicating whether all validatable `FormItem`s are valid. public var isValid: Bool { - elements.compactMap { $0 as? Validatable }.allSatisfy { $0.isValid } + elements + .compactMap { $0 as? (any Validatable) } + .allSatisfy { $0.isValid } } /// Initializes a new `FormView` with the given `FormItem`s. diff --git a/Sources/Forms/Protocols.swift b/Sources/Forms/Protocols.swift index 72b8f56..f34a429 100644 --- a/Sources/Forms/Protocols.swift +++ b/Sources/Forms/Protocols.swift @@ -8,12 +8,15 @@ public protocol FormItem: UIView { /// A protocol that defines a FormItem with input value. public protocol FormInputType: FormItem { + associatedtype Value: Equatable + /// A String value representing the input value of the FormItem. - var value: String? { get } + var value: Value { get } } /// A protocol that defines a validatable FormItem. -public protocol Validatable: FormItem { - /// A Boolean value indicating whether the FormItem is valid. +public protocol Validatable: FormInputType { + + /// A Boolean value indicating whether the item value is valid. var isValid: Bool { get } } diff --git a/Tests/FormsTests/FormCalendarItemTests.swift b/Tests/FormsTests/FormCalendarItemTests.swift new file mode 100644 index 0000000..8063956 --- /dev/null +++ b/Tests/FormsTests/FormCalendarItemTests.swift @@ -0,0 +1,79 @@ +import XCTest +@testable import Forms + +@available(iOS 16.0, *) +final class FormCalendarItemTests: XCTestCase { + func testProperInitialization() { + let configuration = FormCalendarItem.Configuration( + title: [NSAttributedString(string: "mock_title")], + calendar: .init(identifier: .chinese), + tintColor: .red, + spacingAfter: 12, + availableRange: .none, + delegate: nil, + selectionMultiDate: nil, + selectionSingleDate: nil + ) + let calendarItem = FormCalendarItem(configuration: configuration) + + XCTAssertEqual(calendarItem.spacingAfter, 12) + XCTAssertNil(calendarItem.calendarView.delegate) + XCTAssertEqual(calendarItem.titleLabel.numberOfLines, 0) + XCTAssertNil(calendarItem.calendarView.selectionBehavior) + XCTAssertEqual(calendarItem.calendarView.tintColor, .red) + XCTAssertEqual( + calendarItem.calendarView.calendar, + Calendar(identifier: .chinese) + ) + XCTAssertEqual( + calendarItem.calendarView.availableDateRange, + .init(start: .distantPast, end: .distantFuture) + ) + XCTAssertEqual( + calendarItem.titleLabel.attributedText, + NSAttributedString( + string: "mock_title", + attributes: [:] + ) + ) + } + + func testSelectionSingleDateDelegate() { + let singleDelegate = MockSelectionSingleDateDelegate() + singleDelegate.dateSelection( + UICalendarSelectionSingleDate(delegate: nil), + didSelectDate: DateComponents( + year: 2023, + month: 10, + day: 22 + ) + ) + + XCTAssertTrue(singleDelegate.didSelectDateCalled) + XCTAssertEqual(singleDelegate.selectedDate, DateComponents( + year: 2023, + month: 10, + day: 22 + )) + } + + func testInitializationUsingNSCoder() { + let archiver = NSKeyedArchiver(requiringSecureCoding: false) + let item = FormCalendarItem(coder: archiver) + XCTAssertNil(item, "Initialization using NSCoder should return nil") + } +} + +@available(iOS 16.0, *) +final class MockSelectionSingleDateDelegate: NSObject, UICalendarSelectionSingleDateDelegate { + var didSelectDateCalled = false + var selectedDate: DateComponents? + + func dateSelection( + _ selection: UICalendarSelectionSingleDate, + didSelectDate dateComponents: DateComponents? + ) { + didSelectDateCalled = true + selectedDate = dateComponents + } +} diff --git a/Tests/FormsTests/FormInputItemTests.swift b/Tests/FormsTests/FormInputItemTests.swift index 1a23166..9b96b1a 100644 --- a/Tests/FormsTests/FormInputItemTests.swift +++ b/Tests/FormsTests/FormInputItemTests.swift @@ -8,10 +8,9 @@ final class FormInputItemTests: XCTestCase { let initialText = "Initial Text" let item = FormInputItem( configuration: .init( - title: title, - titleAttributes: [:], + title: [NSAttributedString(string: title)], initialText: initialText, - placeholder: placeholder, + placeholder: [NSAttributedString(string: placeholder)], isSecure: false, autocorrectionType: .no, autocapitalizationType: .none, @@ -43,32 +42,6 @@ final class FormInputItemTests: XCTestCase { ) } - func testConstraintsAreActive() { - let item = FormInputItem( - configuration: .init( - title: nil, - titleAttributes: [:], - initialText: nil, - placeholder: nil, - isSecure: false, - autocorrectionType: .default, - autocapitalizationType: .allCharacters, - font: .boldSystemFont(ofSize: 1), - textColor: .black, - cornerRadius: 1, - borderWidth: 1, - borderColor: .black, - spacingAfter: 1, - didChange: nil - ) - ) - item.layoutIfNeeded() // Force layout so constraints are activated. - - for constraint in item.constraints { - XCTAssertTrue(constraint.isActive, "All constraints should be active") - } - } - func testInitializationUsingNSCoder() { let archiver = NSKeyedArchiver(requiringSecureCoding: false) let item = FormInputItem(coder: archiver) diff --git a/Tests/FormsTests/FormTextItemTests.swift b/Tests/FormsTests/FormTextItemTests.swift index c24c076..594b198 100644 --- a/Tests/FormsTests/FormTextItemTests.swift +++ b/Tests/FormsTests/FormTextItemTests.swift @@ -10,9 +10,12 @@ final class FormTextItemTests: XCTestCase { .foregroundColor: UIColor.red ] let item = FormTextItem( - configuration: .init( - text: text, - attributes: attributes, + configuration: FormTextItem.Configuration( + text: [ + NSAttributedString( + string: text, + attributes: attributes + )], spacingAfter: spacing ) ) @@ -32,11 +35,14 @@ final class FormTextItemTests: XCTestCase { let font = UIFont.systemFont(ofSize: 16) let spacing: CGFloat = 15 let item = FormTextItem( - configuration: .init( - text: text, - attributes: [ - .foregroundColor: color, - .font: font + configuration: FormTextItem.Configuration( + text: [ + NSAttributedString( + string: text, + attributes: [ + .font: font, + .foregroundColor: color + ]) ], spacingAfter: spacing ) @@ -127,15 +133,16 @@ private extension FormTextItem { _ string: String, _ spacingAfter: CGFloat = 10 ) -> FormTextItem { - FormTextItem( - configuration: .init( - text: string, - attributes: [ - .foregroundColor: UIColor.black, - .font: UIFont.systemFont(ofSize: 12) - ], - spacingAfter: spacingAfter - ) - ) + FormTextItem(configuration: .init( + text: [ + NSAttributedString( + string: string, + attributes: [ + .foregroundColor: UIColor.black, + .font: UIFont.systemFont(ofSize: 12) + ]) + ], + spacingAfter: spacingAfter + )) } } diff --git a/Tests/FormsTests/FormViewTests.swift b/Tests/FormsTests/FormViewTests.swift index a133a77..44f5c29 100644 --- a/Tests/FormsTests/FormViewTests.swift +++ b/Tests/FormsTests/FormViewTests.swift @@ -109,6 +109,8 @@ final class FormViewTests: XCTestCase { } private final class FormItemMock: UIView, FormItem, Validatable { + var value: String { fatalError() } + var spacingAfter: CGFloat var isValid: Bool