diff --git a/Example/Example/Views/FinishedScreenView.swift b/Example/Example/Views/FinishedScreenView.swift index d5436e9..1c25057 100644 --- a/Example/Example/Views/FinishedScreenView.swift +++ b/Example/Example/Views/FinishedScreenView.swift @@ -13,7 +13,7 @@ final class FinishedScreenView: UIView { init() { super.init(frame: .zero) - backgroundColor = .white + backgroundColor = .systemBackground addSubview(formView) formView.translatesAutoresizingMaskIntoConstraints = false @@ -36,7 +36,7 @@ private extension FormTextItem.Configuration { text: "Horray! You finished the onboarding. 🎉", attributes: [ .font: UIFont(name: "AvenirNext-DemiBold", size: 30)!, - .foregroundColor: UIColor.black, + .foregroundColor: UIColor.label, .kern: 0.5, ], spacingAfter: 20 diff --git a/Example/Example/Views/PermissionsScreenView.swift b/Example/Example/Views/PermissionsScreenView.swift index 6d345bf..d95f24d 100644 --- a/Example/Example/Views/PermissionsScreenView.swift +++ b/Example/Example/Views/PermissionsScreenView.swift @@ -24,7 +24,7 @@ final class PermissionsScreenView: UIView { init() { super.init(frame: .zero) - backgroundColor = .white + backgroundColor = .systemBackground addSubview(formView) formView.translatesAutoresizingMaskIntoConstraints = false @@ -47,7 +47,7 @@ private extension FormTextItem.Configuration { text: "Manage Permissions", attributes: [ .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.black, + .foregroundColor: UIColor.label, .kern: 0.5 ], spacingAfter: 20 @@ -57,7 +57,7 @@ private extension 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.darkGray, + .foregroundColor: UIColor.secondaryLabel, .kern: 0.2 ], spacingAfter: 20 @@ -68,10 +68,10 @@ private extension FormSwitchItem.Configuration { static let first = FormSwitchItem.Configuration( title: "Location Access", titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .black, + 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: .gray, + subtitleColor: .secondaryLabel, onColor: .systemBlue, isOn: false, spacingAfter: 25 @@ -80,10 +80,10 @@ private extension FormSwitchItem.Configuration { static let second = FormSwitchItem.Configuration( title: "Notifications", titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .black, + titleColor: .label, subtitle: "Enable notifications to stay updated with the latest news, updates, and offers.", subtitleFont: UIFont(name: "AvenirNext-Regular", size: 15)!, - subtitleColor: .gray, + subtitleColor: .secondaryLabel, onColor: .systemGreen, isOn: false, spacingAfter: 25 @@ -92,10 +92,10 @@ private extension FormSwitchItem.Configuration { static let third = FormSwitchItem.Configuration( title: "Camera Access", titleFont: UIFont(name: "AvenirNext-Medium", size: 17)!, - titleColor: .black, + 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: .gray, + subtitleColor: .secondaryLabel, onColor: .systemRed, isOn: false, spacingAfter: 25 diff --git a/Example/Example/Views/PersonalScreenView.swift b/Example/Example/Views/PersonalScreenView.swift index 0ffa8da..3190597 100644 --- a/Example/Example/Views/PersonalScreenView.swift +++ b/Example/Example/Views/PersonalScreenView.swift @@ -1,6 +1,11 @@ import Forms import UIKit +typealias CalendarDelegate = +UICalendarViewDelegate & +UICalendarSelectionMultiDateDelegate & +UICalendarSelectionSingleDateDelegate + /// `PersonalScreenView` is a `UIView` subclass that sets up and /// manages `FormItem's` included on the Personal Details UI flow. /// @@ -10,9 +15,10 @@ final class PersonalScreenView: UIView { lazy var formView = FormView(elements: [ FormTextItem(configuration: .title), FormTextItem(configuration: .subtitle), - inputItem, requiredInputItem, numbersInputItem, + inputItem, + calendarItem, FormSpacingItem(), buttonItem ]) @@ -22,10 +28,11 @@ final class PersonalScreenView: UIView { let requiredInputItem = MinimumFormInputItem(configuration: .second) let numbersInputItem = RegexFormInputItem(configuration: .third) let buttonItem = FormButtonItem(configuration: .personal) + lazy var calendarItem = FormCalendarItem(configuration: .personal(delegate: self)) init() { super.init(frame: .zero) - backgroundColor = .white + backgroundColor = .systemBackground addSubview(scrollView) scrollView.addSubview(formView) @@ -53,6 +60,63 @@ final class PersonalScreenView: UIView { required init?(coder: NSCoder) { nil } } +// MARK: - UICalendarSelectionSingleDateDelegate + +extension PersonalScreenView: UICalendarSelectionSingleDateDelegate { + func dateSelection( + _ selection: UICalendarSelectionSingleDate, + didSelectDate dateComponents: DateComponents? + ) { + if let day = dateComponents?.day, + let month = dateComponents?.month, + let year = dateComponents?.year { + print(">>> Did select \(day)/\(month)/\(year)") + } + } +} + +// MARK: - UICalendarSelectionMultiDateDelegate + +extension PersonalScreenView: UICalendarSelectionMultiDateDelegate { + func multiDateSelection( + _ selection: UICalendarSelectionMultiDate, + didSelectDate dateComponents: DateComponents + ) { + if let day = dateComponents.day, + let month = dateComponents.month, + let year = dateComponents.year { + print(">>> Did select \(day)/\(month)/\(year)") + } + } + + func multiDateSelection( + _ selection: UICalendarSelectionMultiDate, + didDeselectDate dateComponents: DateComponents + ) { + if let day = dateComponents.day, + let month = dateComponents.month, + let year = dateComponents.year { + print(">>> Did de-select \(day)/\(month)/\(year)") + } + } +} + +// MARK: - UICalendarViewDelegate + +extension PersonalScreenView: UICalendarViewDelegate { + func calendarView( + _ calendarView: UICalendarView, + decorationFor dateComponents: DateComponents + ) -> UICalendarView.Decoration? { + switch dateComponents.day { + case 5: + return .default(color: .systemRed, size: .small) + default: + return .default(color: .systemGreen, size: .small) + } + } +} + // MARK: - FormItem.Configuration private extension FormTextItem.Configuration { @@ -60,20 +124,20 @@ private extension FormTextItem.Configuration { text: "Your personal details", attributes: [ .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.black, + .foregroundColor: UIColor.label, .kern: 0.5 ], - spacingAfter: 15 + 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.darkGray, + .foregroundColor: UIColor.secondaryLabel, .kern: 0.2 ], - spacingAfter: 30 + spacingAfter: 20 ) } @@ -82,7 +146,7 @@ private extension FormInputItem.Configuration { title: "Street Address", titleAttributes: [ .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.black + .foregroundColor: UIColor.label ], initialText: nil, placeholder: "e.g. 5912 5th Avenue, New York, NY", @@ -90,10 +154,10 @@ private extension FormInputItem.Configuration { autocorrectionType: .no, autocapitalizationType: .none, font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.darkGray, + textColor: UIColor.label, cornerRadius: 8, borderWidth: 1.0, - borderColor: UIColor.lightGray, + borderColor: UIColor.systemGray, spacingAfter: 15, didChange: nil ) @@ -102,7 +166,7 @@ private extension FormInputItem.Configuration { title: "Full Name", titleAttributes: [ .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.black + .foregroundColor: UIColor.label ], initialText: nil, placeholder: "Required", @@ -110,7 +174,7 @@ private extension FormInputItem.Configuration { autocorrectionType: .default, autocapitalizationType: .words, font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.darkGray, + textColor: UIColor.label, cornerRadius: 8, borderWidth: 1.0, borderColor: UIColor.lightGray, @@ -121,8 +185,8 @@ private extension FormInputItem.Configuration { static let third = FormInputItem.Configuration( title: "Phone Number", titleAttributes: [ - .font: UIFont(name: "AvenirNext-Medium", size: 16)!, - .foregroundColor: UIColor.black + .font: UIFont(name: "AvenirNext-Medium", size: 16)!, + .foregroundColor: UIColor.label ], initialText: nil, placeholder: "Only numbers allowed", @@ -130,11 +194,11 @@ private extension FormInputItem.Configuration { autocorrectionType: .no, autocapitalizationType: .none, font: UIFont(name: "AvenirNext-Regular", size: 16)!, - textColor: UIColor.darkGray, + textColor: UIColor.label, cornerRadius: 8, borderWidth: 1.0, borderColor: UIColor.lightGray, - spacingAfter: 20, + spacingAfter: 15, didChange: nil ) } @@ -155,3 +219,35 @@ private extension FormButtonItem.Configuration { shouldBeEnabled: false ) } + +private extension FormCalendarItem.Configuration { + static func personal( + delegate: CalendarDelegate + ) -> FormCalendarItem.Configuration { + FormCalendarItem.Configuration( + title: "Date of Birth", + 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 + ] + ) + } +} + +private extension FormCalendarItem.Configuration.SelectionSingleDate { + static func selectionSingleDate( + delegate: UICalendarSelectionSingleDateDelegate + ) -> FormCalendarItem.Configuration.SelectionSingleDate { + FormCalendarItem.Configuration.SelectionSingleDate( + delegate: delegate, + selectedDate: DateComponents(year: 1995, month: 06, day: 01) + ) + } +} diff --git a/Example/Example/Views/TermsScreenView.swift b/Example/Example/Views/TermsScreenView.swift index 90ae57a..1d5586f 100644 --- a/Example/Example/Views/TermsScreenView.swift +++ b/Example/Example/Views/TermsScreenView.swift @@ -20,7 +20,7 @@ final class TermsScreenView: UIView { init() { super.init(frame: .zero) - backgroundColor = .white + backgroundColor = .systemBackground addSubview(formView) formView.translatesAutoresizingMaskIntoConstraints = false @@ -43,7 +43,7 @@ private extension FormTextItem.Configuration { text: "Review Our Terms & Conditions", attributes: [ .font: UIFont(name: "AvenirNext-DemiBold", size: 24)!, - .foregroundColor: UIColor.black, + .foregroundColor: UIColor.label, .kern: 0.5 ], spacingAfter: 20 @@ -53,7 +53,7 @@ private extension 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.darkGray, + .foregroundColor: UIColor.secondaryLabel, .kern: 0.2 ], spacingAfter: 20 @@ -64,10 +64,10 @@ private extension FormCheckboxItem.Configuration { static let terms = FormCheckboxItem.Configuration( title: "Acceptance of Terms & Conditions", titleFont: UIFont(name: "AvenirNext-Medium", size: 18)!, - titleColor: UIColor.black, + 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.gray, + subtitleColor: UIColor.secondaryLabel, checkedColor: UIColor.systemGreen, uncheckedColor: UIColor.white, borderWidth: 1.0, diff --git a/Sources/Forms/FormCalendarItem.swift b/Sources/Forms/FormCalendarItem.swift new file mode 100644 index 0000000..3792630 --- /dev/null +++ b/Sources/Forms/FormCalendarItem.swift @@ -0,0 +1,153 @@ +import UIKit + +/// `FormCalendarItem` represents a calendar item within a form-based interface. +/// +/// This class is a customizable and interactive component, allowing users +/// to pick and represent dates in form-based UIs. As a subclass of `UIView`, it +/// provides extensive customization options. +/// +/// - Note: This class conforms to the `FormItem` protocol. +@available(iOS 16.0, *) +open class FormCalendarItem: UIView, FormItem { + + /// A structure used to configure a `FormCalendarItem`. + /// + /// It holds all the customizable parameters, which include visual attributes + /// and spacing information for the calendar item in the form. + public struct Configuration { + public struct SelectionSingleDate { + let delegate: UICalendarSelectionSingleDateDelegate? + let selectedDate: DateComponents + + /// Initializes a new instance of `FormCalendarItem.Configuration.SelectionSingleDate`. + /// - Parameters: + /// - delegate: A set of methods to provide selectable dates and handle changes to the selection of a single date. + /// - selectedDate: A date or time specified in terms of units to be evaluated in a calendar system and time zone. + public init( + delegate: UICalendarSelectionSingleDateDelegate?, + selectedDate: DateComponents + ) { + self.delegate = delegate + self.selectedDate = selectedDate + } + } + + public struct SelectionMultiDate { + let delegate: UICalendarSelectionMultiDateDelegate? + let selectedDates: [DateComponents] + + /// Initializes a new instance of `FormCalendarItem.Configuration.SelectionMultiDate`. + /// - Parameters: + /// - delegate: A set of methods to provide selectable dates and handle changes to the selection of multiple dates. + /// - selectedDates: An array of dates or times specified in terms of units to be evaluated in a calendar system and time zone. + public init( + delegate: UICalendarSelectionMultiDateDelegate?, + selectedDates: [DateComponents] + ) { + self.delegate = delegate + self.selectedDates = selectedDates + } + } + + let title: String + let calendar: Calendar + let delegate: UICalendarViewDelegate? + let tintColor: UIColor + let spacingAfter: CGFloat + 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. + /// - 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. + /// - availableRange: The range of dates that the calendar item can display. + /// - 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, + calendar: Calendar, + tintColor: UIColor, + spacingAfter: CGFloat, + availableRange: DateInterval?, + delegate: UICalendarViewDelegate?, + selectionMultiDate: SelectionMultiDate?, + selectionSingleDate: SelectionSingleDate?, + titleAttributes: [NSAttributedString.Key : Any] + ) { + self.title = title + self.calendar = calendar + self.delegate = delegate + 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() + + /// The space after the calendar item in the form. + public var spacingAfter: CGFloat + + public init(configuration: Configuration) { + spacingAfter = configuration.spacingAfter + calendarView.delegate = configuration.delegate + calendarView.calendar = configuration.calendar + calendarView.tintColor = configuration.tintColor + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = NSAttributedString( + string: configuration.title, + attributes: configuration.titleAttributes + ) + + if let availableRange = configuration.availableRange { + calendarView.availableDateRange = availableRange + } + + super.init(frame: .zero) + + if let model = configuration.selectionSingleDate { + let singleDateSelection = UICalendarSelectionSingleDate(delegate: model.delegate) + singleDateSelection.selectedDate = model.selectedDate + calendarView.selectionBehavior = singleDateSelection + } + + if let model = configuration.selectionMultiDate { + let multiDateSelection = UICalendarSelectionMultiDate(delegate: model.delegate) + multiDateSelection.selectedDates = model.selectedDates + calendarView.selectionBehavior = multiDateSelection + } + setupViews() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + addSubview(stackView) + stackView.spacing = 5 + stackView.axis = .vertical + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(calendarView) + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.leftAnchor.constraint(equalTo: leftAnchor), + stackView.rightAnchor.constraint(equalTo: rightAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } +} diff --git a/Sources/Forms/FormTextItem.swift b/Sources/Forms/FormTextItem.swift index 6dfc295..0360cdd 100644 --- a/Sources/Forms/FormTextItem.swift +++ b/Sources/Forms/FormTextItem.swift @@ -63,6 +63,7 @@ open class FormTextItem: UIView, FormItem { /// - font: The font of the text label. /// - color: The text color of the text label. /// - spacingAfter: The space after the text item in the form. + @available(*, deprecated, message: "Use 'init(configuration:)'") public init(text: String, font: UIFont, color: UIColor, spacingAfter: CGFloat) { textLabel.text = text textLabel.font = font diff --git a/Tests/FormsTests/FormTextItemTests.swift b/Tests/FormsTests/FormTextItemTests.swift index c182208..c24c076 100644 --- a/Tests/FormsTests/FormTextItemTests.swift +++ b/Tests/FormsTests/FormTextItemTests.swift @@ -28,14 +28,18 @@ final class FormTextItemTests: XCTestCase { func testInitWithFontColor() { let text = "Test" + let color = UIColor.black let font = UIFont.systemFont(ofSize: 16) - let color = UIColor.blue let spacing: CGFloat = 15 let item = FormTextItem( - text: text, - font: font, - color: color, - spacingAfter: spacing + configuration: .init( + text: text, + attributes: [ + .foregroundColor: color, + .font: font + ], + spacingAfter: spacing + ) ) XCTAssertEqual( item.spacingAfter, spacing, @@ -124,10 +128,14 @@ private extension FormTextItem { _ spacingAfter: CGFloat = 10 ) -> FormTextItem { FormTextItem( - text: string, - font: .systemFont(ofSize: 12), - color: .black, - spacingAfter: spacingAfter + configuration: .init( + text: string, + attributes: [ + .foregroundColor: UIColor.black, + .font: UIFont.systemFont(ofSize: 12) + ], + spacingAfter: spacingAfter + ) ) } }