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