From 4213a99b1e9bbf7908ffd6bbed4c1d4b40c7ca9f Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Sat, 9 Jul 2022 18:58:29 -0300 Subject: [PATCH] Implement animated scroll tab alternating --- .../SearchContainerController.swift | 110 +++++++++++++++ .../Controllers/SearchEmailController.swift | 9 ++ .../Controllers/SearchPhoneController.swift | 9 ++ .../Controllers/SearchQRController.swift | 9 ++ .../SearchUsernameController.swift | 9 ++ .../NewUI/Views/SearchEmailView.swift | 28 ++++ .../NewUI/Views/SearchPhoneView.swift | 28 ++++ .../NewUI/Views/SearchQRView.swift | 28 ++++ .../Views/SearchUsernamePlaceholderView.swift | 20 +++ .../NewUI/Views/SearchUsernameView.swift | 36 +++++ .../Views/SearchContainerView.swift | 11 +- .../Views/SearchSegmentedButton.swift | 27 +++- .../Views/SearchSegmentedControl.swift | 126 ++++++++---------- 13 files changed, 369 insertions(+), 81 deletions(-) create mode 100644 Sources/SearchFeature/NewUI/Controllers/SearchEmailController.swift create mode 100644 Sources/SearchFeature/NewUI/Controllers/SearchPhoneController.swift create mode 100644 Sources/SearchFeature/NewUI/Controllers/SearchQRController.swift create mode 100644 Sources/SearchFeature/NewUI/Controllers/SearchUsernameController.swift create mode 100644 Sources/SearchFeature/NewUI/Views/SearchEmailView.swift create mode 100644 Sources/SearchFeature/NewUI/Views/SearchPhoneView.swift create mode 100644 Sources/SearchFeature/NewUI/Views/SearchQRView.swift create mode 100644 Sources/SearchFeature/NewUI/Views/SearchUsernamePlaceholderView.swift create mode 100644 Sources/SearchFeature/NewUI/Views/SearchUsernameView.swift diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index f42bb1ad..1baedc82 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -1,6 +1,7 @@ import UIKit import Theme import Shared +import Combine import DependencyInjection public final class SearchContainerController: UIViewController { @@ -8,8 +9,16 @@ public final class SearchContainerController: UIViewController { lazy private var screenView = SearchContainerView() + private var cancellables = Set<AnyCancellable>() + private let qrController = SearchQRController() + private let emailController = SearchEmailController() + private let phoneController = SearchPhoneController() + private let usernameController = SearchUsernameController() + public override func loadView() { view = screenView + screenView.scrollView.delegate = self + embedControllers() } public override func viewWillAppear(_ animated: Bool) { @@ -24,6 +33,7 @@ public final class SearchContainerController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() + setupBindings() } private func setupNavigationBar() { @@ -42,7 +52,107 @@ public final class SearchContainerController: UIViewController { ) } + private func setupBindings() { + screenView.segmentedControl + .actionPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + let page = CGFloat($0.rawValue) + let point: CGPoint = CGPoint(x: screenView.frame.width * page, y: 0.0) + screenView.scrollView.setContentOffset(point, animated: true) + }.store(in: &cancellables) + } + @objc private func didTapBack() { navigationController?.popViewController(animated: true) } + + private func embedControllers() { + addChild(qrController) + addChild(emailController) + addChild(phoneController) + addChild(usernameController) + + screenView.scrollView.addSubview(qrController.view) + screenView.scrollView.addSubview(emailController.view) + screenView.scrollView.addSubview(phoneController.view) + screenView.scrollView.addSubview(usernameController.view) + + usernameController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) + $0.left.equalToSuperview() + $0.right.equalTo(emailController.view.snp.left) + } + + emailController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) + $0.right.equalTo(phoneController.view.snp.left) + } + + phoneController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) + $0.right.equalTo(qrController.view.snp.left) + } + + qrController.view.snp.makeConstraints { + $0.top.equalTo(screenView.segmentedControl.snp.bottom) + $0.width.equalTo(screenView) + $0.bottom.equalTo(screenView) + } + + qrController.didMove(toParent: self) + emailController.didMove(toParent: self) + phoneController.didMove(toParent: self) + usernameController.didMove(toParent: self) + } +} + +extension SearchContainerController: UIScrollViewDelegate { + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let pageOffset = scrollView.contentOffset.x / view.frame.width + scrollSegmentedControlTrack(using: pageOffset) + updateSegmentedControlButtonsColor(using: pageOffset) + } + + private func scrollSegmentedControlTrack(using pageOffset: CGFloat) { + let amountOfTabs = 4.0 + let tabWidth = screenView.bounds.width / amountOfTabs + + if let leftConstraint = screenView.segmentedControl.leftConstraint { + leftConstraint.update(offset: pageOffset * tabWidth) + } + } + + private func updateSegmentedControlButtonsColor(using pageOffset: CGFloat) { + let qrRate = highlightRateFor(page: 3, offset: pageOffset) + let emailRate = highlightRateFor(page: 1, offset: pageOffset) + let phoneRate = highlightRateFor(page: 2, offset: pageOffset) + let usernameRate = highlightRateFor(page: 0, offset: pageOffset) + + screenView.segmentedControl.qrCodeButton.updateHighlighting(rate: qrRate) + screenView.segmentedControl.emailButton.updateHighlighting(rate: emailRate) + screenView.segmentedControl.phoneButton.updateHighlighting(rate: phoneRate) + screenView.segmentedControl.usernameButton.updateHighlighting(rate: usernameRate) + } + + private func highlightRateFor(page: CGFloat, offset: CGFloat) -> CGFloat { + let lowerBound = page - 1 + let upperBound = page + 1 + + if offset > lowerBound && offset < upperBound { + if (offset - lowerBound) > 1 { + return 1 - (offset - page) + } else { + return offset - lowerBound + } + } else { + return 0 + } + } } diff --git a/Sources/SearchFeature/NewUI/Controllers/SearchEmailController.swift b/Sources/SearchFeature/NewUI/Controllers/SearchEmailController.swift new file mode 100644 index 00000000..1380e7f1 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Controllers/SearchEmailController.swift @@ -0,0 +1,9 @@ +import UIKit + +final class SearchEmailController: UIViewController { + lazy private var screenView = SearchEmailView() + + override func loadView() { + view = screenView + } +} diff --git a/Sources/SearchFeature/NewUI/Controllers/SearchPhoneController.swift b/Sources/SearchFeature/NewUI/Controllers/SearchPhoneController.swift new file mode 100644 index 00000000..63d6f568 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Controllers/SearchPhoneController.swift @@ -0,0 +1,9 @@ +import UIKit + +final class SearchPhoneController: UIViewController { + lazy private var screenView = SearchPhoneView() + + override func loadView() { + view = screenView + } +} diff --git a/Sources/SearchFeature/NewUI/Controllers/SearchQRController.swift b/Sources/SearchFeature/NewUI/Controllers/SearchQRController.swift new file mode 100644 index 00000000..04fe440f --- /dev/null +++ b/Sources/SearchFeature/NewUI/Controllers/SearchQRController.swift @@ -0,0 +1,9 @@ +import UIKit + +final class SearchQRController: UIViewController { + lazy private var screenView = SearchQRView() + + override func loadView() { + view = screenView + } +} diff --git a/Sources/SearchFeature/NewUI/Controllers/SearchUsernameController.swift b/Sources/SearchFeature/NewUI/Controllers/SearchUsernameController.swift new file mode 100644 index 00000000..136951c8 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Controllers/SearchUsernameController.swift @@ -0,0 +1,9 @@ +import UIKit + +final class SearchUsernameController: UIViewController { + lazy private var screenView = SearchUsernameView() + + override func loadView() { + view = screenView + } +} diff --git a/Sources/SearchFeature/NewUI/Views/SearchEmailView.swift b/Sources/SearchFeature/NewUI/Views/SearchEmailView.swift new file mode 100644 index 00000000..053c6259 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Views/SearchEmailView.swift @@ -0,0 +1,28 @@ +import UIKit +import Shared +import InputField + +final class SearchEmailView: UIView { + let inputField = InputField() + + init() { + super.init(frame: .zero) + + inputField.setup( + style: .regular, + title: "Email", + placeholder: "Email" + ) + + addSubview(inputField) + + inputField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/NewUI/Views/SearchPhoneView.swift b/Sources/SearchFeature/NewUI/Views/SearchPhoneView.swift new file mode 100644 index 00000000..1868cec6 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Views/SearchPhoneView.swift @@ -0,0 +1,28 @@ +import UIKit +import Shared +import InputField + +final class SearchPhoneView: UIView { + let inputField = InputField() + + init() { + super.init(frame: .zero) + + inputField.setup( + style: .regular, + title: "Phone", + placeholder: "Phone" + ) + + addSubview(inputField) + + inputField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/NewUI/Views/SearchQRView.swift b/Sources/SearchFeature/NewUI/Views/SearchQRView.swift new file mode 100644 index 00000000..4adae045 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Views/SearchQRView.swift @@ -0,0 +1,28 @@ +import UIKit +import Shared +import InputField + +final class SearchQRView: UIView { + let inputField = InputField() + + init() { + super.init(frame: .zero) + + inputField.setup( + style: .regular, + title: "QR", + placeholder: "QR" + ) + + addSubview(inputField) + + inputField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/NewUI/Views/SearchUsernamePlaceholderView.swift b/Sources/SearchFeature/NewUI/Views/SearchUsernamePlaceholderView.swift new file mode 100644 index 00000000..effcf607 --- /dev/null +++ b/Sources/SearchFeature/NewUI/Views/SearchUsernamePlaceholderView.swift @@ -0,0 +1,20 @@ +import UIKit +import Shared + +final class SearchUsernamePlaceholderView: UIView { + let titleLabel = UILabel() + + init() { + super.init(frame: .zero) + + titleLabel.text = "[SearchUsernamePlaceholderView]" + + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/NewUI/Views/SearchUsernameView.swift b/Sources/SearchFeature/NewUI/Views/SearchUsernameView.swift new file mode 100644 index 00000000..2dd66fcd --- /dev/null +++ b/Sources/SearchFeature/NewUI/Views/SearchUsernameView.swift @@ -0,0 +1,36 @@ +import UIKit +import Shared +import InputField + +final class SearchUsernameView: UIView { + let inputField = InputField() + let placeholderView = SearchUsernamePlaceholderView() + + init() { + super.init(frame: .zero) + + inputField.setup( + style: .regular, + title: "Username", + placeholder: "Username" + ) + + addSubview(inputField) + addSubview(placeholderView) + + inputField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + } + + placeholderView.snp.makeConstraints { + $0.top.equalTo(inputField.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/Views/SearchContainerView.swift b/Sources/SearchFeature/Views/SearchContainerView.swift index 5b350905..a1f2b9ba 100644 --- a/Sources/SearchFeature/Views/SearchContainerView.swift +++ b/Sources/SearchFeature/Views/SearchContainerView.swift @@ -12,16 +12,19 @@ final class SearchContainerView: UIView { addSubview(segmentedControl) addSubview(scrollView) - scrollView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - segmentedControl.snp.makeConstraints { $0.top.equalTo(safeAreaLayoutGuide).offset(10) $0.left.equalToSuperview() $0.right.equalToSuperview() $0.height.equalTo(60) } + + scrollView.snp.makeConstraints { + $0.top.equalTo(segmentedControl.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } } required init?(coder: NSCoder) { nil } diff --git a/Sources/SearchFeature/Views/SearchSegmentedButton.swift b/Sources/SearchFeature/Views/SearchSegmentedButton.swift index 1142ead7..3237777b 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedButton.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedButton.swift @@ -2,8 +2,10 @@ import UIKit import Shared final class SearchSegmentedButton: UIControl { - let titleLabel = UILabel() - let imageView = UIImageView() + private let titleLabel = UILabel() + private let imageView = UIImageView() + private let highlightColor = Asset.brandPrimary.color + private let discreteColor = Asset.neutralDisabled.color init() { super.init(frame: .zero) @@ -30,12 +32,25 @@ final class SearchSegmentedButton: UIControl { required init?(coder: NSCoder) { nil } - func setup(title: String, icon: UIImage) { - titleLabel.text = title - imageView.image = icon + func setup( + title: String, + icon: UIImage, + iconColor: UIColor = Asset.neutralDisabled.color, + titleColor: UIColor = Asset.neutralDisabled.color + ) { + self.imageView.image = icon + self.titleLabel.text = title + self.imageView.tintColor = iconColor + self.titleLabel.textColor = titleColor } - func update(color: UIColor) { + func updateHighlighting(rate: CGFloat) { + let color = UIColor.fade( + from: discreteColor, + to: highlightColor, + pcent: rate + ) + imageView.tintColor = color titleLabel.textColor = color } diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift index af0e4271..212c78e6 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedControl.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift @@ -1,53 +1,88 @@ import UIKit import Shared import SnapKit +import Combine final class SearchSegmentedControl: UIView { + enum Item: Int { + case username = 0 + case email + case phone + case qr + } + private let trackView = UIView() private let stackView = UIStackView() - private var leftConstraint: Constraint? private let trackIndicatorView = UIView() + private(set) var leftConstraint: Constraint? private(set) var usernameButton = SearchSegmentedButton() private(set) var emailButton = SearchSegmentedButton() private(set) var phoneButton = SearchSegmentedButton() private(set) var qrCodeButton = SearchSegmentedButton() + var actionPublisher: AnyPublisher<Item, Never> { + actionSubject.eraseToAnyPublisher() + } + + private var cancellables = Set<AnyCancellable>() + private let actionSubject = PassthroughSubject<Item, Never>() + init() { super.init(frame: .zero) trackView.backgroundColor = Asset.neutralLine.color trackIndicatorView.backgroundColor = Asset.brandPrimary.color - qrCodeButton.titleLabel.text = Localized.Ud.Tab.qr - emailButton.titleLabel.text = Localized.Ud.Tab.email - phoneButton.titleLabel.text = Localized.Ud.Tab.phone - usernameButton.titleLabel.text = Localized.Ud.Tab.username - - usernameButton.titleLabel.textColor = Asset.brandPrimary.color - emailButton.titleLabel.textColor = Asset.neutralDisabled.color - phoneButton.titleLabel.textColor = Asset.neutralDisabled.color - qrCodeButton.titleLabel.textColor = Asset.neutralDisabled.color - - usernameButton.imageView.tintColor = Asset.brandPrimary.color - emailButton.imageView.tintColor = Asset.neutralDisabled.color - phoneButton.imageView.tintColor = Asset.neutralDisabled.color - qrCodeButton.imageView.tintColor = Asset.neutralDisabled.color + usernameButton.setup( + title: Localized.Ud.Tab.username, + icon: Asset.searchTabUsername.image, + iconColor: Asset.brandPrimary.color, + titleColor: Asset.brandPrimary.color + ) - qrCodeButton.imageView.image = Asset.searchTabQr.image - emailButton.imageView.image = Asset.searchTabEmail.image - phoneButton.imageView.image = Asset.searchTabPhone.image - usernameButton.imageView.image = Asset.searchTabUsername.image + qrCodeButton.setup(title: Localized.Ud.Tab.qr, icon: Asset.searchTabQr.image) + emailButton.setup(title: Localized.Ud.Tab.email, icon: Asset.searchTabEmail.image) + phoneButton.setup(title: Localized.Ud.Tab.phone, icon: Asset.searchTabPhone.image) + stackView.distribution = .fillEqually stackView.addArrangedSubview(usernameButton) stackView.addArrangedSubview(emailButton) stackView.addArrangedSubview(phoneButton) stackView.addArrangedSubview(qrCodeButton) - stackView.distribution = .fillEqually stackView.backgroundColor = Asset.neutralWhite.color addSubview(stackView) addSubview(trackView) trackView.addSubview(trackIndicatorView) + setupBindings() + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + private func setupBindings() { + usernameButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.username) } + .store(in: &cancellables) + + emailButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.email) } + .store(in: &cancellables) + + phoneButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.phone) } + .store(in: &cancellables) + + qrCodeButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in actionSubject.send(.qr) } + .store(in: &cancellables) + } + + private func setupConstraints() { stackView.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -66,55 +101,4 @@ final class SearchSegmentedControl: UIView { $0.bottom.equalToSuperview() } } - - required init?(coder: NSCoder) { nil } - - func updateSwipePercentage(_ percentageScrolled: CGFloat) { - let amountOfTabs = 4.0 - let tabWidth = bounds.width / amountOfTabs - let leftOffset = percentageScrolled * tabWidth - - leftConstraint?.update(offset: leftOffset) - - let usernamePercentage = percentageScrolled > 1 ? 1 : percentageScrolled - let phonePercentage = percentageScrolled <= 1 ? 0 : percentageScrolled - 1 - let emailPercentage = percentageScrolled > 1 ? 1 - (percentageScrolled-1) : percentageScrolled - let qrPercentage = percentageScrolled > 1 ? 1 - (percentageScrolled-1) : percentageScrolled - - let usernameColor = UIColor.fade( - from: Asset.brandPrimary.color, - to: Asset.neutralDisabled.color, - pcent: usernamePercentage - ) - - let emailColor = UIColor.fade( - from: Asset.neutralDisabled.color, - to: Asset.brandPrimary.color, - pcent: emailPercentage - ) - - let phoneColor = UIColor.fade( - from: Asset.neutralDisabled.color, - to: Asset.brandPrimary.color, - pcent: phonePercentage - ) - - let qrColor = UIColor.fade( - from: Asset.brandPrimary.color, - to: Asset.neutralDisabled.color, - pcent: qrPercentage - ) - - usernameButton.imageView.tintColor = usernameColor - usernameButton.titleLabel.textColor = usernameColor - - emailButton.imageView.tintColor = emailColor - emailButton.titleLabel.textColor = emailColor - - phoneButton.imageView.tintColor = phoneColor - phoneButton.titleLabel.textColor = phoneColor - - qrCodeButton.imageView.tintColor = qrColor - qrCodeButton.titleLabel.textColor = qrColor - } } -- GitLab