From a4c22bcfb9e32e5f1ddb73c015690504a3d23cc0 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 4 Aug 2023 15:20:27 +0200 Subject: [PATCH 01/10] Make actions container reusable --- .../View/CardTokenizationViewController.swift | 6 +-- .../View/POCardTokenizationStyle.swift | 6 +-- .../CardTokenizationViewModelState.swift | 4 +- .../DefaultCardTokenizationViewModel.swift | 2 + ...AlternativePaymentMethodActionsStyle.swift | 39 ++------------ ...ONativeAlternativePaymentMethodStyle.swift | 6 +-- ...ternativePaymentMethodViewController.swift | 8 +-- ...iveAlternativePaymentMethodViewModel.swift | 2 + ...ternativePaymentMethodViewModelState.swift | 25 +-------- .../ActionsContainerView.swift} | 54 ++++++++----------- .../ActionsContainerViewModel.swift | 33 ++++++++++++ .../POActionsContainerStyle.swift | 42 +++++++++++++++ 12 files changed, 121 insertions(+), 106 deletions(-) rename Sources/ProcessOut/Sources/UI/{Modules/NativeAlternativePaymentMethod/View/Buttons/NativeAlternativePaymentMethodButtonsView.swift => Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift} (67%) create mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift create mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 6b8bc3da2..a1fd63556 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -237,7 +237,7 @@ final class CardTokenizationViewController return view }() - private lazy var buttonsContainerView = NativeAlternativePaymentMethodButtonsView( + private lazy var buttonsContainerView = ActionsContainerView( style: style.actions, horizontalInset: Constants.contentInset.left ) @@ -295,7 +295,7 @@ final class CardTokenizationViewController self?.updateFirstResponder() } UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - buttonsContainerView.configure(actions: state.actions, animated: animated) + buttonsContainerView.configure(viewModel: state.actions, animated: animated) collectionOverlayView.layoutIfNeeded() } } @@ -408,7 +408,7 @@ final class CardTokenizationViewController private func configureCollectionViewBottomInset(state: ViewModel.State) { let bottomInset = Constants.contentInset.bottom + keyboardHeight - + buttonsContainerView.contentHeight(actions: state.actions) + + buttonsContainerView.contentHeight(viewModel: state.actions) if bottomInset != collectionView.contentInset.bottom { collectionView.contentInset.bottom = bottomInset } diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift index 7e5c8cae7..a17390e3f 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift @@ -24,7 +24,7 @@ public struct POCardTokenizationStyle { public let errorDescription: POTextStyle /// Actions style. - public let actions: PONativeAlternativePaymentMethodActionsStyle + public let actions: POActionsContainerStyle /// Background color. public let backgroundColor: UIColor @@ -37,7 +37,7 @@ public struct POCardTokenizationStyle { sectionTitle: POTextStyle? = nil, input: POInputStyle? = nil, errorDescription: POTextStyle? = nil, - actions: PONativeAlternativePaymentMethodActionsStyle? = nil, + actions: POActionsContainerStyle? = nil, backgroundColor: UIColor? = nil, separatorColor: UIColor? = nil ) { @@ -57,7 +57,7 @@ public struct POCardTokenizationStyle { static let sectionTitle = POTextStyle(color: Asset.Colors.Text.secondary.color, typography: .Fixed.labelHeading) static let input = POInputStyle.default() static let errorDescription = POTextStyle(color: Asset.Colors.Text.error.color, typography: .Fixed.label) - static let actions = PONativeAlternativePaymentMethodActionsStyle() + static let actions = POActionsContainerStyle() static let backgroundColor = Asset.Colors.Surface.level1.color static let separatorColor = Asset.Colors.Border.subtle.color } diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index 46ac4e2e5..bae7aa1ce 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -80,8 +80,8 @@ struct CardTokenizationViewModelState { let items: [Item] } - typealias Action = NativeAlternativePaymentMethodViewModelState.Action - typealias Actions = NativeAlternativePaymentMethodViewModelState.Actions + typealias Action = ActionsContainerActionViewModel + typealias Actions = ActionsContainerViewModel /// Available items. let sections: [Section] diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index 5ddd6afb7..4bccc5b0a 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -168,6 +168,7 @@ final class DefaultCardTokenizationViewModel: BaseViewModel() collectionViewDataSource.applySnapshotUsingReloadData(snapshot) view.backgroundColor = style.background.regular @@ -334,7 +334,7 @@ final class NativeAlternativePaymentMethodViewController Void - } - - struct Actions { - - /// Primary action. - let primary: Action? - - /// Secondary action. - let secondary: Action? - } + typealias Action = ActionsContainerActionViewModel + typealias Actions = ActionsContainerViewModel struct Started { diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Buttons/NativeAlternativePaymentMethodButtonsView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift similarity index 67% rename from Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Buttons/NativeAlternativePaymentMethodButtonsView.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift index 661a8e6d2..53a579b42 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Buttons/NativeAlternativePaymentMethodButtonsView.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift @@ -7,9 +7,9 @@ import UIKit -final class NativeAlternativePaymentMethodButtonsView: UIView { +final class ActionsContainerView: UIView { - init(style: PONativeAlternativePaymentMethodActionsStyle, horizontalInset: CGFloat) { + init(style: POActionsContainerStyle, horizontalInset: CGFloat) { self.style = style self.horizontalInset = horizontalInset super.init(frame: .zero) @@ -21,19 +21,19 @@ final class NativeAlternativePaymentMethodButtonsView: UIView { fatalError("init(coder:) has not been implemented") } - func configure(actions: NativeAlternativePaymentMethodViewModelState.Actions, animated: Bool) { - if actions.primary != nil || actions.secondary != nil { + func configure(viewModel: ActionsContainerViewModel, animated: Bool) { + if viewModel.primary != nil || viewModel.secondary != nil { let animated = animated && alpha > 0 - configure(button: primaryButton, withAction: actions.primary, animated: animated) - configure(button: secondaryButton, withAction: actions.secondary, animated: animated) + configure(button: primaryButton, viewModel: viewModel.primary, animated: animated) + configure(button: secondaryButton, viewModel: viewModel.secondary, animated: animated) alpha = 1 } else { alpha = 0 } } - func contentHeight(actions: NativeAlternativePaymentMethodViewModelState.Actions) -> CGFloat { - guard actions.primary != nil || actions.secondary != nil else { + func contentHeight(viewModel: ActionsContainerViewModel) -> CGFloat { + guard viewModel.primary != nil || viewModel.secondary != nil else { return 0 } let numberOfActions: Int @@ -41,7 +41,7 @@ final class NativeAlternativePaymentMethodButtonsView: UIView { case .horizontal: numberOfActions = 1 case .vertical: - numberOfActions = [actions.primary, actions.secondary].compactMap { $0 }.count + numberOfActions = [viewModel.primary, viewModel.secondary].compactMap { $0 }.count @unknown default: assertionFailure("Unexpected axis.") numberOfActions = 1 @@ -67,7 +67,7 @@ final class NativeAlternativePaymentMethodButtonsView: UIView { // MARK: - Private Properties - private let style: PONativeAlternativePaymentMethodActionsStyle + private let style: POActionsContainerStyle private let horizontalInset: CGFloat private lazy var contentView: UIStackView = { @@ -90,17 +90,8 @@ final class NativeAlternativePaymentMethodButtonsView: UIView { return view }() - private lazy var primaryButton: Button = { - let button = Button(style: style.primary) - button.accessibilityIdentifier = "native-alternative-payment.primary-button" - return button - }() - - private lazy var secondaryButton: Button = { - let button = Button(style: style.secondary) - button.accessibilityIdentifier = "native-alternative-payment.secondary-button" - return button - }() + private lazy var primaryButton = Button(style: style.primary) + private lazy var secondaryButton = Button(style: style.secondary) private lazy var bottomConstraint = contentView.bottomAnchor.constraint( equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -Constants.verticalInset @@ -129,19 +120,18 @@ final class NativeAlternativePaymentMethodButtonsView: UIView { backgroundColor = style.backgroundColor } - private func configure( - button: Button, withAction action: NativeAlternativePaymentMethodViewModelState.Action?, animated: Bool - ) { - guard let action else { + private func configure(button: Button, viewModel: ActionsContainerActionViewModel?, animated: Bool) { + if let viewModel { + let buttonViewModel = Button.ViewModel( + title: viewModel.title, isLoading: viewModel.isExecuting, handler: viewModel.handler + ) + button.configure(viewModel: buttonViewModel, isEnabled: viewModel.isEnabled, animated: animated) + button.setHidden(false) + button.alpha = 1 + button.accessibilityIdentifier = viewModel.accessibilityIdentifier + } else { button.setHidden(true) button.alpha = 0 - return } - let viewModel = Button.ViewModel( - title: action.title, isLoading: action.isExecuting, handler: action.handler - ) - button.configure(viewModel: viewModel, isEnabled: action.isEnabled, animated: animated) - button.setHidden(false) - button.alpha = 1 } } diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift new file mode 100644 index 000000000..8add6aca7 --- /dev/null +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift @@ -0,0 +1,33 @@ +// +// ActionsContainerViewModel.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 04.08.2023. +// + +struct ActionsContainerViewModel { + + /// Primary action. + let primary: ActionsContainerActionViewModel? + + /// Secondary action. + let secondary: ActionsContainerActionViewModel? +} + +struct ActionsContainerActionViewModel { + + /// Action title. + let title: String + + /// Boolean value indicating whether action is enabled. + let isEnabled: Bool + + /// Boolean value indicating whether action associated with button is currently running. + let isExecuting: Bool + + /// Accessibility identifier. + let accessibilityIdentifier: String + + /// Action handler. + let handler: () -> Void +} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift new file mode 100644 index 000000000..5275ed01b --- /dev/null +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift @@ -0,0 +1,42 @@ +// +// POActionsContainerStyle.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 22.05.2023. +// + +import UIKit + +/// Actions container style. +public struct POActionsContainerStyle { + + /// Style for primary button. + public let primary: POButtonStyle + + /// Style for secondary button. + public let secondary: POButtonStyle + + /// The axis along which the buttons lay out. By default actions are positioned vertically. + public let axis: NSLayoutConstraint.Axis + + /// Container separator color. + public let separatorColor: UIColor + + /// Container background color. + public let backgroundColor: UIColor + + /// Creates style instance. + public init( + primary: POButtonStyle? = nil, + secondary: POButtonStyle? = nil, + axis: NSLayoutConstraint.Axis? = nil, + separatorColor: UIColor? = nil, + backgroundColor: UIColor? = nil + ) { + self.primary = primary ?? .primary + self.secondary = secondary ?? .secondary + self.axis = axis ?? .vertical + self.separatorColor = separatorColor ?? Asset.Colors.Border.subtle.color + self.backgroundColor = backgroundColor ?? Asset.Colors.Surface.level1.color + } +} From adb3d1b509f727699ce8c4467b4fe329c1da227f Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 4 Aug 2023 15:20:56 +0200 Subject: [PATCH 02/10] Update doc --- Sources/ProcessOut/ProcessOut.docc/ProcessOut.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md b/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md index 748a3e683..8086a0a0f 100644 --- a/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md +++ b/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md @@ -110,6 +110,7 @@ Types that describe properties such as shadow and border. And style of higher le - ``POButtonStateStyle`` - ``POActivityIndicatorStyle`` - ``POActivityIndicatorView`` +- ``POActionsContainerStyle`` ### Utils From 81430f94196a5e4addfecd99424c49ec6f8ecd35 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 7 Aug 2023 10:50:40 +0200 Subject: [PATCH 03/10] Fix folder name --- .../ActionsContainerView.swift | 0 .../ActionsContainerViewModel.swift | 0 .../POActionsContainerStyle.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/{ButtonsContainer => ActionsContainer}/ActionsContainerView.swift (100%) rename Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/{ButtonsContainer => ActionsContainer}/ActionsContainerViewModel.swift (100%) rename Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/{ButtonsContainer => ActionsContainer}/POActionsContainerStyle.swift (100%) diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift similarity index 100% rename from Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerView.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift similarity index 100% rename from Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/ActionsContainerViewModel.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift similarity index 100% rename from Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ButtonsContainer/POActionsContainerStyle.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift From df20c81f44c58360063f97160a1468f3213e1692 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 7 Aug 2023 11:06:56 +0200 Subject: [PATCH 04/10] Extract center layout --- .../View/CardTokenizationViewController.swift | 12 ++++++------ ...ativeAlternativePaymentMethodViewController.swift | 10 +++++----- .../Layouts/Center/CollectionViewCenterLayout.swift} | 10 +++++----- .../Center/CollectionViewDelegateCenterLayout.swift} | 5 ++--- 4 files changed, 18 insertions(+), 19 deletions(-) rename Sources/ProcessOut/Sources/UI/{Modules/NativeAlternativePaymentMethod/View/Layout/NativeAlternativePaymentMethodCollectionLayout.swift => Shared/DesignSystem/Layouts/Center/CollectionViewCenterLayout.swift} (95%) rename Sources/ProcessOut/Sources/UI/{Modules/NativeAlternativePaymentMethod/View/Layout/NativeAlternativePaymentMethodCollectionLayoutDelegate.swift => Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift} (66%) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index a1fd63556..56c1d69da 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -11,7 +11,7 @@ import UIKit final class CardTokenizationViewController: BaseViewController, - NativeAlternativePaymentMethodCollectionLayoutDelegate, + CollectionViewDelegateCenterLayout, CardTokenizationCellDelegate { init(viewModel: ViewModel, style: POCardTokenizationStyle, logger: POLogger) { @@ -89,7 +89,7 @@ final class CardTokenizationViewController collectionOverlayView.layoutIfNeeded() } - // MARK: - NativeAlternativePaymentMethodCollectionLayoutDelegate + // MARK: - CollectionViewDelegateCenterLayout func centeredSection(layout: UICollectionViewLayout) -> Int? { nil @@ -251,8 +251,8 @@ final class CardTokenizationViewController return collectionView }() - private lazy var collectionViewLayout: NativeAlternativePaymentMethodCollectionLayout = { - let layout = NativeAlternativePaymentMethodCollectionLayout() + private lazy var collectionViewLayout: CollectionViewCenterLayout = { + let layout = CollectionViewCenterLayout() layout.minimumLineSpacing = Constants.itemsSpacing layout.minimumInteritemSpacing = Constants.itemsSpacing return layout @@ -355,7 +355,7 @@ final class CardTokenizationViewController ) collectionView.registerSupplementaryView( CardTokenizationSeparatorView.self, - kind: NativeAlternativePaymentMethodCollectionLayout.elementKindSeparator + kind: CollectionViewCenterLayout.elementKindSeparator ) collectionView.registerCell(CardTokenizationTitleCell.self) collectionView.registerCell(CardTokenizationInputCell.self) @@ -385,7 +385,7 @@ final class CardTokenizationViewController return nil } switch kind { - case NativeAlternativePaymentMethodCollectionLayout.elementKindSeparator: + case CollectionViewCenterLayout.elementKindSeparator: let view = collectionView.dequeueReusableSupplementaryView( CardTokenizationSeparatorView.self, kind: kind, indexPath: indexPath ) diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift index 788d8c1ae..2135311b6 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift @@ -11,7 +11,7 @@ import UIKit final class NativeAlternativePaymentMethodViewController: BaseViewController, - NativeAlternativePaymentMethodCollectionLayoutDelegate, + CollectionViewDelegateCenterLayout, NativeAlternativePaymentMethodCellDelegate { init(viewModel: ViewModel, style: PONativeAlternativePaymentMethodStyle, logger: POLogger) { @@ -87,7 +87,7 @@ final class NativeAlternativePaymentMethodViewController Int? { let snapshot = collectionViewDataSource.snapshot() @@ -289,7 +289,7 @@ final class NativeAlternativePaymentMethodViewController = { @@ -394,7 +394,7 @@ final class NativeAlternativePaymentMethodViewController NativeAlternativePaymentMethodCollectionLayoutDelegate { + private func delegate() -> CollectionViewDelegateCenterLayout { // swiftlint:disable:next force_cast force_unwrapping - return collectionView!.delegate as! NativeAlternativePaymentMethodCollectionLayoutDelegate + return collectionView!.delegate as! CollectionViewDelegateCenterLayout } private func collectionView() -> UICollectionView { @@ -205,7 +205,7 @@ final class NativeAlternativePaymentMethodCollectionLayout: UICollectionViewFlow } } -extension NativeAlternativePaymentMethodCollectionLayout { +extension CollectionViewCenterLayout { /// Section background element kind. static let elementKindSeparator = "ElementKindSeparator" diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Layout/NativeAlternativePaymentMethodCollectionLayoutDelegate.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift similarity index 66% rename from Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Layout/NativeAlternativePaymentMethodCollectionLayoutDelegate.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift index fb25848dd..678439d74 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Layout/NativeAlternativePaymentMethodCollectionLayoutDelegate.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift @@ -1,5 +1,5 @@ // -// NativeAlternativePaymentMethodCollectionLayoutDelegate.swift +// CollectionViewDelegateCenterLayout.swift // ProcessOut // // Created by Andrii Vysotskyi on 01.05.2023. @@ -7,8 +7,7 @@ import UIKit -// swiftlint:disable:next type_name -protocol NativeAlternativePaymentMethodCollectionLayoutDelegate: AnyObject, UICollectionViewDelegateFlowLayout { +protocol CollectionViewDelegateCenterLayout: AnyObject, UICollectionViewDelegateFlowLayout { /// Should return index of the section that should be centered. func centeredSection(layout: UICollectionViewLayout) -> Int? From 4557f9241c009e96cd52848e50e5ce63d3dd7b97 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 7 Aug 2023 13:21:43 +0200 Subject: [PATCH 05/10] Extract formatting logic --- .../Cells/CardTokenizationInputCell.swift | 26 +----------- ...iveAlternativePaymentMethodInputCell.swift | 26 +----------- .../UI/Shared/Utils/TextFieldUtils.swift | 42 +++++++++++++++++++ 3 files changed, 44 insertions(+), 50 deletions(-) create mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift index 249ae6d27..ca8160eef 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift @@ -121,30 +121,6 @@ extension CardTokenizationInputCell: UITextFieldDelegate { guard let formatter = item?.formatter else { return true } - // swiftlint:disable legacy_objc_type - let originalString = (textField.text ?? "") as NSString - var updatedString = originalString.replacingCharacters(in: range, with: string) as NSString - // swiftlint:enable legacy_objc_type - var proposedSelectedRange = NSRange(location: updatedString.length, length: 0) - let isReplacementValid = formatter.isPartialStringValid( - &updatedString, - proposedSelectedRange: &proposedSelectedRange, - originalString: originalString as String, - originalSelectedRange: range, - errorDescription: nil - ) - guard isReplacementValid else { - return false - } - textField.text = updatedString as String - // swiftlint:disable:next line_length - if let position = textField.position(from: textField.beginningOfDocument, offset: proposedSelectedRange.lowerBound) { - // fixme(andrii-vysotskyi): when called as a result of paste system changes our selection to wrong value - // based on length of `replacementString` after call textField(:shouldChangeCharactersIn:replacementString:) - // returns, even if this method returns false. - textField.selectedTextRange = textField.textRange(from: position, to: position) - } - textField.sendActions(for: .editingChanged) - return false + return TextFieldUtils.changeText(in: range, replacement: string, textField: textField, formatter: formatter) } } diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift index 9af30930b..66478c6fe 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift @@ -139,30 +139,6 @@ extension NativeAlternativePaymentMethodInputCell: UITextFieldDelegate { guard let formatter = item?.formatter else { return true } - // swiftlint:disable legacy_objc_type - let originalString = (textField.text ?? "") as NSString - var updatedString = originalString.replacingCharacters(in: range, with: string) as NSString - // swiftlint:enable legacy_objc_type - var proposedSelectedRange = NSRange(location: updatedString.length, length: 0) - let isReplacementValid = formatter.isPartialStringValid( - &updatedString, - proposedSelectedRange: &proposedSelectedRange, - originalString: originalString as String, - originalSelectedRange: range, - errorDescription: nil - ) - guard isReplacementValid else { - return false - } - textField.text = updatedString as String - // swiftlint:disable:next line_length - if let position = textField.position(from: textField.beginningOfDocument, offset: proposedSelectedRange.lowerBound) { - // fixme(andrii-vysotskyi): when called as a result of paste system changes our selection to wrong value - // based on length of `replacementString` after call textField(:shouldChangeCharactersIn:replacementString:) - // returns, even if this method returns false. - textField.selectedTextRange = textField.textRange(from: position, to: position) - } - textField.sendActions(for: .editingChanged) - return false + return TextFieldUtils.changeText(in: range, replacement: string, textField: textField, formatter: formatter) } } diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift new file mode 100644 index 000000000..434a18428 --- /dev/null +++ b/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift @@ -0,0 +1,42 @@ +// +// TextFieldUtils.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 07.08.2023. +// + +import Foundation +import UIKit + +enum TextFieldUtils { + + static func changeText( + in range: NSRange, replacement string: String, textField: UITextField, formatter: Formatter + ) -> Bool { + // swiftlint:disable legacy_objc_type + let originalString = (textField.text ?? "") as NSString + var updatedString = originalString.replacingCharacters(in: range, with: string) as NSString + // swiftlint:enable legacy_objc_type + var proposedSelectedRange = NSRange(location: updatedString.length, length: 0) + let isReplacementValid = formatter.isPartialStringValid( + &updatedString, + proposedSelectedRange: &proposedSelectedRange, + originalString: originalString as String, + originalSelectedRange: range, + errorDescription: nil + ) + guard isReplacementValid else { + return false + } + textField.text = updatedString as String + // swiftlint:disable:next line_length + if let position = textField.position(from: textField.beginningOfDocument, offset: proposedSelectedRange.lowerBound) { + // fixme(andrii-vysotskyi): when called as a result of paste system changes our selection to wrong value + // based on length of `replacementString` after call textField(:shouldChangeCharactersIn:replacementString:) + // returns, even if this method returns false. + textField.selectedTextRange = textField.textRange(from: position, to: position) + } + textField.sendActions(for: .editingChanged) + return false + } +} From 9635393ee1f134975a92b12a5f4023e903d9cf7b Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 7 Aug 2023 13:36:12 +0200 Subject: [PATCH 06/10] Rework editing availability --- .../View/CardTokenizationViewController.swift | 4 ++++ .../View/Cells/CardTokenizationCell.swift | 3 +++ .../View/Cells/CardTokenizationInputCell.swift | 2 +- .../ViewModel/CardTokenizationViewModelState.swift | 4 ---- .../DefaultCardTokenizationViewModel.swift | 14 ++++++-------- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 56c1d69da..10eec91c5 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -221,6 +221,10 @@ final class CardTokenizationViewController return true } + func cardTokenizationCellShouldBeginEditing(_ cell: CardTokenizationCell) -> Bool { + viewModel.state.isEditingAllowed + } + // MARK: - Private Nested Types private typealias SectionIdentifier = ViewModel.State.SectionIdentifier diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift index 378ac3a03..25f02dcab 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift @@ -26,4 +26,7 @@ protocol CardTokenizationCellDelegate: AnyObject { /// Should return boolean value indicating whether cells input should return ie resign first responder. func cardTokenizationCellShouldReturn(_ cell: CardTokenizationCell) -> Bool + + /// Should return boolean value indicating whether editing is currently allowed. + func cardTokenizationCellShouldBeginEditing(_ cell: CardTokenizationCell) -> Bool } diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift index ca8160eef..a5de730e9 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift @@ -108,7 +108,7 @@ final class CardTokenizationInputCell: UICollectionViewCell, CardTokenizationCel extension CardTokenizationInputCell: UITextFieldDelegate { func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - item?.value.isEditingAllowed ?? false + delegate?.cardTokenizationCellShouldBeginEditing(self) ?? false } func textFieldShouldReturn(_ textField: UITextField) -> Bool { diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index bae7aa1ce..f74a37fcb 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -19,10 +19,6 @@ struct CardTokenizationViewModelState { /// Boolean value indicating whether value is valid. @ReferenceWrapper var isInvalid: Bool - - /// Boolean value indicating whether editing is allowed. - @ReferenceWrapper - var isEditingAllowed: Bool } struct TitleItem: Hashable { diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index 4bccc5b0a..225804737 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -108,7 +108,7 @@ final class DefaultCardTokenizationViewModel: BaseViewModel [State.Item] { let number = State.InputItem( placeholder: Text.CardDetails.Number.placeholder, - value: inputValue(for: startedState.number, isEditingAllowed: isEditingAllowed), + value: inputValue(for: startedState.number), formatter: cardNumberFormatter, isCompact: false, keyboard: .asciiCapableNumberPad, @@ -116,7 +116,7 @@ final class DefaultCardTokenizationViewModel: BaseViewModel State.InputValue { + private func inputValue(for parameter: InteractorState.Parameter) -> State.InputValue { if let value = inputValuesCache[parameter.id] { value.text = parameter.value value.isInvalid = !parameter.isValid - value.isEditingAllowed = isEditingAllowed return value } let value = State.InputValue( text: .init(value: parameter.value), - isInvalid: .init(value: !parameter.isValid), - isEditingAllowed: .init(value: isEditingAllowed) + isInvalid: .init(value: !parameter.isValid) ) let observer = value.$text.addObserver { [weak self] value in self?.interactor.update(parameterId: parameter.id, value: value) From 3597d8c02d36999d5222e2dc87559b81cb7ff0f9 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 8 Aug 2023 11:51:56 +0200 Subject: [PATCH 07/10] Rework responders handling --- .../CardTokenizationInteractorState.swift | 5 + .../DefaultCardTokenizationInteractor.swift | 7 +- .../View/CardTokenizationViewController.swift | 96 ++----------------- .../View/Cells/CardTokenizationCell.swift | 15 --- .../Cells/CardTokenizationInputCell.swift | 60 ++++++++---- .../ViewModel/CardTokenizationViewModel.swift | 4 +- .../CardTokenizationViewModelState.swift | 8 ++ .../DefaultCardTokenizationViewModel.swift | 84 ++++++++++++---- 8 files changed, 132 insertions(+), 147 deletions(-) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift index 4215d3ae0..5cc7221cd 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift @@ -5,6 +5,8 @@ // Created by Andrii Vysotskyi on 18.07.2023. // +import Foundation + enum CardTokenizationInteractorState { typealias ParameterId = WritableKeyPath @@ -19,6 +21,9 @@ enum CardTokenizationInteractorState { /// Indicates whether parameter is valid. var isValid = true + + /// Formatter that can be used to format this parameter. + var formatter: Formatter? } struct Started { diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift index d6c7068e6..bbaa94194 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift @@ -26,8 +26,8 @@ final class DefaultCardTokenizationInteractor: return } let startedState = State.Started( - number: .init(id: \.number), - expiration: .init(id: \.expiration), + number: .init(id: \.number, formatter: cardNumberFormatter), + expiration: .init(id: \.expiration, formatter: cardExpirationFormatter), cvc: .init(id: \.cvc), cardholderName: .init(id: \.cardholderName) ) @@ -38,7 +38,8 @@ final class DefaultCardTokenizationInteractor: guard case .started(var startedState) = state, startedState[keyPath: parameterId].value != value else { return } - startedState[keyPath: parameterId] = .init(id: parameterId, value: value, isValid: true) + startedState[keyPath: parameterId].value = value + startedState[keyPath: parameterId].isValid = true if areParametersValid(startedState: startedState) { startedState.recentErrorMessage = nil } diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 10eec91c5..630ecaa41 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -5,20 +5,18 @@ // Created by Andrii Vysotskyi on 24.07.2023. // -// swiftlint:disable type_body_length file_length +// swiftlint:disable type_body_length import UIKit final class CardTokenizationViewController: BaseViewController, - CollectionViewDelegateCenterLayout, - CardTokenizationCellDelegate { + CollectionViewDelegateCenterLayout { init(viewModel: ViewModel, style: POCardTokenizationStyle, logger: POLogger) { self.style = style self.logger = logger keyboardHeight = 0 - didAppear = false super.init(viewModel: viewModel, logger: logger) } @@ -31,8 +29,7 @@ final class CardTokenizationViewController override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - didAppear = true - updateFirstResponder() + viewModel.didAppear() } override func loadView() { @@ -191,40 +188,6 @@ final class CardTokenizationViewController return sectionInset } - // MARK: - Scroll View Delegate - - func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - updateFirstResponder() - } - - // MARK: - CardTokenizationCellDelegate - - func cardTokenizationCellShouldReturn(_ cell: CardTokenizationCell) -> Bool { - let visibleIndexPaths = collectionView.indexPathsForVisibleItems.sorted() - guard let indexPath = collectionView.indexPath(for: cell), - let nextIndex = visibleIndexPaths.firstIndex(of: indexPath)?.advanced(by: 1), - visibleIndexPaths.indices.contains(nextIndex) else { - viewModel.submit() - return true - } - for indexPath in visibleIndexPaths.suffix(from: nextIndex) { - guard let cell = collectionView.cellForItem(at: indexPath) as? CardTokenizationCell, - let responder = cell.inputResponder else { - continue - } - if responder.becomeFirstResponder() { - collectionView.scrollToItem(at: indexPath, at: .top, animated: true) - } - return true - } - viewModel.submit() - return true - } - - func cardTokenizationCellShouldBeginEditing(_ cell: CardTokenizationCell) -> Bool { - viewModel.state.isEditingAllowed - } - // MARK: - Private Nested Types private typealias SectionIdentifier = ViewModel.State.SectionIdentifier @@ -278,7 +241,6 @@ final class CardTokenizationViewController }() private var keyboardHeight: CGFloat - private var didAppear: Bool // MARK: - State Management @@ -295,59 +257,14 @@ final class CardTokenizationViewController if reload { snapshot.reloadSections(collectionViewDataSource.snapshot().sectionIdentifiers) } - collectionViewDataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in - self?.updateFirstResponder() - } + collectionViewDataSource.apply(snapshot, animatingDifferences: animated) UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in buttonsContainerView.configure(viewModel: state.actions, animated: animated) collectionOverlayView.layoutIfNeeded() } - } - - // MARK: - Current Responder Handling - - private func updateFirstResponder() { - // Becoming first responder may cause UI issues related to keyboard presentation if attempted - // before view appears on screen. For example, during a push to UINavigationController. So - // the operation is delayed until then. - guard didAppear else { - return - } - if !viewModel.state.isEditingAllowed { - logger.debug("Editing is not allowed in current state, will resign first responder") + if !state.isEditingAllowed { view.endEditing(true) - return - } - let isEditing = collectionView.indexPathsForVisibleItems.contains { indexPath in - let cell = collectionView.cellForItem(at: indexPath) as? CardTokenizationCell - return cell?.inputResponder?.isFirstResponder == true - } - guard !isEditing, let indexPath = indexPathForFutureFirstResponderCell() else { - return - } - if collectionView.indexPathsForVisibleItems.contains(indexPath) { - let cell = collectionView.cellForItem(at: indexPath) as? CardTokenizationCell - cell?.inputResponder?.becomeFirstResponder() - } - collectionView.scrollToItem(at: indexPath, at: .top, animated: true) - } - - private func indexPathForFutureFirstResponderCell() -> IndexPath? { - let snapshot = collectionViewDataSource.snapshot() - var inputsIndexPaths: [IndexPath] = [] - for (section, sectionId) in snapshot.sectionIdentifiers.enumerated() { - for (row, item) in snapshot.itemIdentifiers(inSection: sectionId).enumerated() { - guard case .input(let inputItem) = item else { - continue - } - let indexPath = IndexPath(row: row, section: section) - if inputItem.value.isInvalid { - return indexPath - } - inputsIndexPaths.append(indexPath) - } } - return inputsIndexPaths.first } // MARK: - @@ -375,7 +292,6 @@ final class CardTokenizationViewController case .input(let item): let cell = collectionView.dequeueReusableCell(CardTokenizationInputCell.self, for: indexPath) cell.configure(item: item, style: style.input) - cell.delegate = self return cell case .error(let item): let cell = collectionView.dequeueReusableCell(CardTokenizationErrorCell.self, for: indexPath) @@ -427,4 +343,4 @@ private enum Constants { static let inputHeight: CGFloat = 44 } -// swiftlint:enable type_body_length file_length +// swiftlint:enable type_body_length diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift index 25f02dcab..74641c94c 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationCell.swift @@ -14,19 +14,4 @@ protocol CardTokenizationCell: UICollectionViewCell { /// Tells the cell that it was removed from the collection view. func didEndDisplaying() - - /// Should return input responder if any. - var inputResponder: UIResponder? { get } - - /// Cell delegate. - var delegate: CardTokenizationCellDelegate? { get set } -} - -protocol CardTokenizationCellDelegate: AnyObject { - - /// Should return boolean value indicating whether cells input should return ie resign first responder. - func cardTokenizationCellShouldReturn(_ cell: CardTokenizationCell) -> Bool - - /// Should return boolean value indicating whether editing is currently allowed. - func cardTokenizationCellShouldBeginEditing(_ cell: CardTokenizationCell) -> Bool } diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift index a5de730e9..11b8d0a4a 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationInputCell.swift @@ -42,30 +42,14 @@ final class CardTokenizationInputCell: UICollectionViewCell, CardTokenizationCel // MARK: - CardTokenizationCell func willDisplay() { - guard let item, let style else { - return - } - let isInvalidObserver = item.value.$isInvalid.addObserver { [weak self] isInvalid in - self?.textFieldContainer.configure(isInvalid: isInvalid, style: style, animated: true) - } - let valueObserver = item.value.$text.addObserver { [weak self] updatedValue in - if self?.textFieldContainer.textField.text != updatedValue { - self?.textFieldContainer.textField.text = updatedValue - } - } - self.observations = [isInvalidObserver, valueObserver] + observeItemChanges() + updateFirstResponder() } func didEndDisplaying() { observations = [] } - var inputResponder: UIResponder? { - textFieldContainer.textField - } - - weak var delegate: CardTokenizationCellDelegate? - // MARK: - Private Nested Types private enum Constants { @@ -77,6 +61,7 @@ final class CardTokenizationInputCell: UICollectionViewCell, CardTokenizationCel private lazy var textFieldContainer: TextFieldContainerView = { let view = TextFieldContainerView() view.textField.clearButtonMode = .whileEditing + // todo(andrii-vysotskyi): make accessibility identifier dynamic view.textField.accessibilityIdentifier = Constants.accessibilityIdentifier view.textField.delegate = self view.textField.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged) @@ -103,16 +88,49 @@ final class CardTokenizationInputCell: UICollectionViewCell, CardTokenizationCel private func textFieldEditingChanged() { item?.value.text = textFieldContainer.textField.text ?? "" } + + private func observeItemChanges() { + guard let item, let style else { + return + } + let isInvalidObserver = item.value.$isInvalid.addObserver { [weak self] isInvalid in + self?.textFieldContainer.configure(isInvalid: isInvalid, style: style, animated: true) + } + let valueObserver = item.value.$text.addObserver { [weak self] updatedValue in + if self?.textFieldContainer.textField.text != updatedValue { + self?.textFieldContainer.textField.text = updatedValue + } + } + let activityObserver = item.value.$isFocused.addObserver { [weak self] _ in + self?.updateFirstResponder() + } + self.observations = [isInvalidObserver, valueObserver, activityObserver] + } + + private func updateFirstResponder() { + // Explicitly resigning responder and activating another causes the keyboard to jump. So implementation is + // ignoring it. It is a responsibility of owning controller/collection to end editing when appropriate. + let textField = textFieldContainer.textField + guard let item, item.value.isFocused, !textField.isFirstResponder, window != nil else { + return + } + textField.becomeFirstResponder() + } } extension CardTokenizationInputCell: UITextFieldDelegate { - func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - delegate?.cardTokenizationCellShouldBeginEditing(self) ?? false + func textFieldDidBeginEditing(_ textField: UITextField) { + item?.value.isFocused = true + } + + func textFieldDidEndEditing(_ textField: UITextField) { + item?.value.isFocused = false } func textFieldShouldReturn(_ textField: UITextField) -> Bool { - delegate?.cardTokenizationCellShouldReturn(self) ?? true + item?.submit() + return true } func textField( diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModel.swift index 7f9f0674a..45a94310f 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModel.swift @@ -7,8 +7,8 @@ protocol CardTokenizationViewModel: ViewModel { - /// Submits tokenization request. - func submit() + /// Invoked when view appears on screen. + func didAppear() } // TODOs: diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index f74a37fcb..d8285b8d0 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -19,6 +19,10 @@ struct CardTokenizationViewModelState { /// Boolean value indicating whether value is valid. @ReferenceWrapper var isInvalid: Bool + + /// Boolean value indicating whether input is currently focused. + @ReferenceWrapper + var isFocused: Bool } struct TitleItem: Hashable { @@ -46,6 +50,10 @@ struct CardTokenizationViewModelState { /// Text content type. let contentType: UITextContentType? + + /// Submit items value. + @ImmutableNullHashable + var submit: () -> Void } struct ErrorItem: Hashable { diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index 225804737..e97be16f5 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -22,8 +22,9 @@ final class DefaultCardTokenizationViewModel: BaseViewModel [State.Item] { + let submit: () -> Void = { [weak self] in + self?.onParameterSubmit() + } let number = State.InputItem( placeholder: Text.CardDetails.Number.placeholder, value: inputValue(for: startedState.number), - formatter: cardNumberFormatter, + formatter: startedState.number.formatter, isCompact: false, keyboard: .asciiCapableNumberPad, - contentType: .creditCardNumber + contentType: .creditCardNumber, + submit: submit ) let expiration = State.InputItem( placeholder: Text.CardDetails.Expiration.placeholder, value: inputValue(for: startedState.expiration), - formatter: cardExpirationFormatter, + formatter: startedState.expiration.formatter, isCompact: true, keyboard: .asciiCapableNumberPad, - contentType: nil + contentType: nil, + submit: submit ) let cvc = State.InputItem( placeholder: Text.CardDetails.Cvc.placeholder, value: inputValue(for: startedState.cvc), - formatter: nil, + formatter: startedState.cvc.formatter, isCompact: true, keyboard: .asciiCapableNumberPad, - contentType: nil + contentType: nil, + submit: submit ) let cardholder = State.InputItem( placeholder: Text.CardDetails.Cvc.cardholder, value: inputValue(for: startedState.cardholderName), - formatter: nil, + formatter: startedState.cardholderName.formatter, isCompact: false, keyboard: .asciiCapable, - contentType: .name + contentType: .name, + submit: submit ) return [.input(number), .input(expiration), .input(cvc), .input(cardholder)] } @@ -145,20 +156,48 @@ final class DefaultCardTokenizationViewModel: BaseViewModel State.Action { @@ -190,4 +229,17 @@ final class DefaultCardTokenizationViewModel: BaseViewModel Date: Tue, 8 Aug 2023 15:22:24 +0200 Subject: [PATCH 08/10] Extract tokenization cells --- .../View/CardTokenizationViewController.swift | 22 +++++++++---------- .../{ => Style}/POCardTokenizationStyle.swift | 0 .../CardTokenizationViewModelState.swift | 14 +++--------- .../Error/CollectionViewErrorCell.swift} | 6 ++--- .../Error/CollectionViewErrorViewModel.swift | 12 ++++++++++ .../CollectionViewSectionHeaderView.swift} | 4 ++-- .../CollectionViewSeparatorView.swift} | 4 ++-- .../Title/CollectionViewTitleCell.swift} | 6 ++--- .../Title/CollectionViewTitleViewModel.swift | 12 ++++++++++ 9 files changed, 48 insertions(+), 32 deletions(-) rename Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/{ => Style}/POCardTokenizationStyle.swift (100%) rename Sources/ProcessOut/Sources/UI/{Modules/CardTokenization/View/Cells/CardTokenizationErrorCell.swift => Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift} (89%) create mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift rename Sources/ProcessOut/Sources/UI/{Modules/CardTokenization/View/Supplementary/CardTokenizationSectionHeaderView.swift => Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift} (93%) rename Sources/ProcessOut/Sources/UI/{Modules/CardTokenization/View/Supplementary/CardTokenizationSeparatorView.swift => Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift} (59%) rename Sources/ProcessOut/Sources/UI/{Modules/CardTokenization/View/Cells/CardTokenizationTitleCell.swift => Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift} (90%) create mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 630ecaa41..8083c5143 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -124,7 +124,7 @@ final class CardTokenizationViewController switch collectionViewDataSource.itemIdentifier(for: indexPath) { case .title(let item): height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CardTokenizationTitleCell.self, + viewType: CollectionViewTitleCell.self, preferredWidth: adjustedBounds.width, configure: { cell in cell.configure(item: item, style: self.style.title) @@ -132,7 +132,7 @@ final class CardTokenizationViewController ).height case .error(let item): height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CardTokenizationErrorCell.self, + viewType: CollectionViewErrorCell.self, preferredWidth: adjustedBounds.width, configure: { cell in cell.configure(item: item, style: self.style.errorDescription) @@ -160,7 +160,7 @@ final class CardTokenizationViewController } let width = collectionView.bounds.inset(by: collectionView.adjustedContentInset).width return collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CardTokenizationSectionHeaderView.self, + viewType: CollectionViewSectionHeaderView.self, preferredWidth: width, configure: { [self] view in view.configure(item: sectionTitle, style: style.sectionTitle) @@ -272,21 +272,21 @@ final class CardTokenizationViewController private func configureCollectionView() { _ = collectionViewDataSource collectionView.registerSupplementaryView( - CardTokenizationSectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader + CollectionViewSectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader ) collectionView.registerSupplementaryView( - CardTokenizationSeparatorView.self, + CollectionViewSeparatorView.self, kind: CollectionViewCenterLayout.elementKindSeparator ) - collectionView.registerCell(CardTokenizationTitleCell.self) + collectionView.registerCell(CollectionViewTitleCell.self) + collectionView.registerCell(CollectionViewErrorCell.self) collectionView.registerCell(CardTokenizationInputCell.self) - collectionView.registerCell(CardTokenizationErrorCell.self) } private func cell(for item: ItemIdentifier, at indexPath: IndexPath) -> UICollectionViewCell? { switch item { case .title(let item): - let cell = collectionView.dequeueReusableCell(CardTokenizationTitleCell.self, for: indexPath) + let cell = collectionView.dequeueReusableCell(CollectionViewTitleCell.self, for: indexPath) cell.configure(item: item, style: style.title) return cell case .input(let item): @@ -294,7 +294,7 @@ final class CardTokenizationViewController cell.configure(item: item, style: style.input) return cell case .error(let item): - let cell = collectionView.dequeueReusableCell(CardTokenizationErrorCell.self, for: indexPath) + let cell = collectionView.dequeueReusableCell(CollectionViewErrorCell.self, for: indexPath) cell.configure(item: item, style: style.errorDescription) return cell } @@ -307,7 +307,7 @@ final class CardTokenizationViewController switch kind { case CollectionViewCenterLayout.elementKindSeparator: let view = collectionView.dequeueReusableSupplementaryView( - CardTokenizationSeparatorView.self, kind: kind, indexPath: indexPath + CollectionViewSeparatorView.self, kind: kind, indexPath: indexPath ) view.configure(color: style.separatorColor) return view @@ -316,7 +316,7 @@ final class CardTokenizationViewController return nil } let view = collectionView.dequeueReusableSupplementaryView( - CardTokenizationSectionHeaderView.self, kind: kind, indexPath: indexPath + CollectionViewSectionHeaderView.self, kind: kind, indexPath: indexPath ) view.configure(item: sectionTitle, style: style.sectionTitle) return view diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Style/POCardTokenizationStyle.swift similarity index 100% rename from Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/POCardTokenizationStyle.swift rename to Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Style/POCardTokenizationStyle.swift diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index d8285b8d0..a9e91f231 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -10,6 +10,8 @@ import UIKit struct CardTokenizationViewModelState { + typealias TitleItem = CollectionViewTitleViewModel + struct InputValue: Hashable { /// Current parameter's value text. @@ -25,12 +27,6 @@ struct CardTokenizationViewModelState { var isFocused: Bool } - struct TitleItem: Hashable { - - /// Title text. - let text: String - } - struct InputItem: Hashable { /// Parameter's placeholder. @@ -56,11 +52,7 @@ struct CardTokenizationViewModelState { var submit: () -> Void } - struct ErrorItem: Hashable { - - /// Error description. - let description: String - } + typealias ErrorItem = CollectionViewErrorViewModel enum Item: Hashable { case title(TitleItem), input(InputItem), error(ErrorItem) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationErrorCell.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift similarity index 89% rename from Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationErrorCell.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift index cb7a63c2e..c366ae1e9 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationErrorCell.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift @@ -1,5 +1,5 @@ // -// CardTokenizationErrorCell.swift +// CollectionViewErrorCell.swift // ProcessOut // // Created by Andrii Vysotskyi on 24.07.2023. @@ -7,7 +7,7 @@ import UIKit -final class CardTokenizationErrorCell: UICollectionViewCell { +final class CollectionViewErrorCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) @@ -19,7 +19,7 @@ final class CardTokenizationErrorCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func configure(item: CardTokenizationViewModelState.ErrorItem, style: POTextStyle) { + func configure(item: CollectionViewErrorViewModel, style: POTextStyle) { descriptionLabel.attributedText = AttributedStringBuilder() .with { builder in builder.typography = style.typography diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift new file mode 100644 index 000000000..ce129025a --- /dev/null +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift @@ -0,0 +1,12 @@ +// +// CollectionViewErrorViewModel.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 08.08.2023. +// + +struct CollectionViewErrorViewModel: Hashable { + + /// Error description. + let description: String +} diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSectionHeaderView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift similarity index 93% rename from Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSectionHeaderView.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift index b129ef509..a5bee202c 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSectionHeaderView.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift @@ -1,5 +1,5 @@ // -// CardTokenizationSectionHeaderView.swift +// CollectionViewSectionHeaderView.swift // ProcessOut // // Created by Andrii Vysotskyi on 24.07.2023. @@ -7,7 +7,7 @@ import UIKit -final class CardTokenizationSectionHeaderView: UICollectionReusableView { +final class CollectionViewSectionHeaderView: UICollectionReusableView { override init(frame: CGRect) { super.init(frame: frame) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSeparatorView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift similarity index 59% rename from Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSeparatorView.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift index d941c9339..4ca1d3dd6 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Supplementary/CardTokenizationSeparatorView.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift @@ -1,5 +1,5 @@ // -// CardTokenizationSeparatorView.swift +// CollectionViewSeparatorView.swift // ProcessOut // // Created by Andrii Vysotskyi on 24.07.2023. @@ -7,7 +7,7 @@ import UIKit -final class CardTokenizationSeparatorView: UICollectionReusableView { +final class CollectionViewSeparatorView: UICollectionReusableView { func configure(color: UIColor) { backgroundColor = color diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationTitleCell.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift similarity index 90% rename from Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationTitleCell.swift rename to Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift index 62d5ad5f2..46823b494 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/Cells/CardTokenizationTitleCell.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift @@ -1,5 +1,5 @@ // -// CardTokenizationTitleCell.swift +// CollectionViewTitleCell.swift // ProcessOut // // Created by Andrii Vysotskyi on 24.07.2023. @@ -7,7 +7,7 @@ import UIKit -final class CardTokenizationTitleCell: UICollectionViewCell { +final class CollectionViewTitleCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) @@ -19,7 +19,7 @@ final class CardTokenizationTitleCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func configure(item: CardTokenizationViewModelState.TitleItem, style: POTextStyle) { + func configure(item: CollectionViewTitleViewModel, style: POTextStyle) { titleLabel.attributedText = AttributedStringBuilder() .with { builder in builder.typography = style.typography diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift new file mode 100644 index 000000000..ec9e8de9b --- /dev/null +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift @@ -0,0 +1,12 @@ +// +// CollectionViewTitleViewModel.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 08.08.2023. +// + +struct CollectionViewTitleViewModel: Hashable { + + /// Title text. + let text: String +} From 4bab12b7b3bc217b0ebc270671c21e4bb1b8aaaf Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 8 Aug 2023 15:49:12 +0200 Subject: [PATCH 09/10] Extract native APM views --- .../View/CardTokenizationViewController.swift | 10 ++-- .../CardTokenizationViewModelState.swift | 6 +- .../DefaultCardTokenizationViewModel.swift | 7 ++- ...iveAlternativePaymentMethodErrorCell.swift | 58 ------------------ ...iveAlternativePaymentMethodTitleCell.swift | 59 ------------------- ...ternativePaymentMethodViewController.swift | 23 ++++---- ...nativePaymentMethodSectionHeaderView.swift | 58 ------------------ ...lternativePaymentMethodSeparatorView.swift | 15 ----- ...ternativePaymentMethodViewModelState.swift | 28 ++------- .../Error/CollectionViewErrorCell.swift | 1 + .../Error/CollectionViewErrorViewModel.swift | 3 + .../CollectionViewSectionHeaderView.swift | 5 +- ...CollectionViewSectionHeaderViewModel.swift | 15 +++++ 13 files changed, 51 insertions(+), 237 deletions(-) delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodErrorCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodTitleCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Supplementary/NativeAlternativePaymentMethodSectionHeaderView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Supplementary/NativeAlternativePaymentMethodSeparatorView.swift create mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderViewModel.swift diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 8083c5143..8db0105d5 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -155,7 +155,7 @@ final class CardTokenizationViewController referenceSizeForHeaderInSection section: Int ) -> CGSize { let sectionIdentifier = collectionViewDataSource.snapshot().sectionIdentifiers[section] - guard let sectionTitle = sectionIdentifier.title else { + guard let sectionHeader = sectionIdentifier.header else { return .zero } let width = collectionView.bounds.inset(by: collectionView.adjustedContentInset).width @@ -163,7 +163,7 @@ final class CardTokenizationViewController viewType: CollectionViewSectionHeaderView.self, preferredWidth: width, configure: { [self] view in - view.configure(item: sectionTitle, style: style.sectionTitle) + view.configure(item: sectionHeader, style: style.sectionTitle) } ) } @@ -175,7 +175,7 @@ final class CardTokenizationViewController ) -> UIEdgeInsets { let snapshot = collectionViewDataSource.snapshot() var sectionInset = Constants.sectionInset - if snapshot.sectionIdentifiers[section].title == nil { + if snapshot.sectionIdentifiers[section].header == nil { // Top inset purpose is to add spacing between header and items, // for sections without header instead is 0 sectionInset.top = 0 @@ -312,13 +312,13 @@ final class CardTokenizationViewController view.configure(color: style.separatorColor) return view case UICollectionView.elementKindSectionHeader: - guard let sectionTitle = sectionIdentifier.title else { + guard let sectionHeader = sectionIdentifier.header else { return nil } let view = collectionView.dequeueReusableSupplementaryView( CollectionViewSectionHeaderView.self, kind: kind, indexPath: indexPath ) - view.configure(item: sectionTitle, style: style.sectionTitle) + view.configure(item: sectionHeader, style: style.sectionTitle) return view default: return nil diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index a9e91f231..6528a4dec 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -58,13 +58,15 @@ struct CardTokenizationViewModelState { case title(TitleItem), input(InputItem), error(ErrorItem) } + typealias SectionHeader = CollectionViewSectionHeaderViewModel + struct SectionIdentifier: Hashable { /// Section id. let id: String - /// Section title if any. - let title: String? + /// Section header if any. + let header: SectionHeader? } struct Section { diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index e97be16f5..765351795 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -80,11 +80,12 @@ final class DefaultCardTokenizationViewModel: BaseViewModel Date: Tue, 8 Aug 2023 16:00:52 +0200 Subject: [PATCH 10/10] Fix naming --- .../View/CardTokenizationViewController.swift | 12 ++++++------ ...ativeAlternativePaymentMethodViewController.swift | 12 ++++++------ .../Error/CollectionViewErrorCell.swift | 6 +++--- .../CollectionViewSectionHeaderView.swift | 6 +++--- .../Title/CollectionViewTitleCell.swift | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift index 8db0105d5..4af884f77 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/CardTokenization/View/CardTokenizationViewController.swift @@ -127,7 +127,7 @@ final class CardTokenizationViewController viewType: CollectionViewTitleCell.self, preferredWidth: adjustedBounds.width, configure: { cell in - cell.configure(item: item, style: self.style.title) + cell.configure(viewModel: item, style: self.style.title) } ).height case .error(let item): @@ -135,7 +135,7 @@ final class CardTokenizationViewController viewType: CollectionViewErrorCell.self, preferredWidth: adjustedBounds.width, configure: { cell in - cell.configure(item: item, style: self.style.errorDescription) + cell.configure(viewModel: item, style: self.style.errorDescription) } ).height case .input(let item): @@ -163,7 +163,7 @@ final class CardTokenizationViewController viewType: CollectionViewSectionHeaderView.self, preferredWidth: width, configure: { [self] view in - view.configure(item: sectionHeader, style: style.sectionTitle) + view.configure(viewModel: sectionHeader, style: style.sectionTitle) } ) } @@ -287,7 +287,7 @@ final class CardTokenizationViewController switch item { case .title(let item): let cell = collectionView.dequeueReusableCell(CollectionViewTitleCell.self, for: indexPath) - cell.configure(item: item, style: style.title) + cell.configure(viewModel: item, style: style.title) return cell case .input(let item): let cell = collectionView.dequeueReusableCell(CardTokenizationInputCell.self, for: indexPath) @@ -295,7 +295,7 @@ final class CardTokenizationViewController return cell case .error(let item): let cell = collectionView.dequeueReusableCell(CollectionViewErrorCell.self, for: indexPath) - cell.configure(item: item, style: style.errorDescription) + cell.configure(viewModel: item, style: style.errorDescription) return cell } } @@ -318,7 +318,7 @@ final class CardTokenizationViewController let view = collectionView.dequeueReusableSupplementaryView( CollectionViewSectionHeaderView.self, kind: kind, indexPath: indexPath ) - view.configure(item: sectionHeader, style: style.sectionTitle) + view.configure(viewModel: sectionHeader, style: style.sectionTitle) return view default: return nil diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift index 2ccf021d9..7b4eb6298 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift @@ -143,7 +143,7 @@ final class NativeAlternativePaymentMethodViewController