Skip to content
Snippets Groups Projects
Commit 61c7d1f3 authored by Bruno Muniz's avatar Bruno Muniz :apple:
Browse files

Merge branch 'search-tabs-scroll' into 'development'

Implement animated scroll tab alternating

See merge request elixxir/client-ios!47
parents 2184f1bf 93feddc1
No related branches found
No related tags found
2 merge requests!54Releasing 1.1.4,!47Implement animated scroll tab alternating
Showing
with 460 additions and 844 deletions
import UIKit import UIKit
import Theme import Theme
import Shared import Shared
import Combine
import DependencyInjection import DependencyInjection
public final class SearchContainerController: UIViewController { public final class SearchContainerController: UIViewController {
...@@ -8,8 +9,16 @@ public final class SearchContainerController: UIViewController { ...@@ -8,8 +9,16 @@ public final class SearchContainerController: UIViewController {
lazy private var screenView = SearchContainerView() 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() { public override func loadView() {
view = screenView view = screenView
screenView.scrollView.delegate = self
embedControllers()
} }
public override func viewWillAppear(_ animated: Bool) { public override func viewWillAppear(_ animated: Bool) {
...@@ -24,6 +33,7 @@ public final class SearchContainerController: UIViewController { ...@@ -24,6 +33,7 @@ public final class SearchContainerController: UIViewController {
public override func viewDidLoad() { public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupNavigationBar() setupNavigationBar()
setupBindings()
} }
private func setupNavigationBar() { private func setupNavigationBar() {
...@@ -42,7 +52,107 @@ public final class SearchContainerController: UIViewController { ...@@ -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() { @objc private func didTapBack() {
navigationController?.popViewController(animated: true) 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
}
}
} }
import HUD
import Theme
import UIKit
import Shared
import Models
import Combine
import Defaults
import XXModels
import Countries
import DrawerFeature
import DependencyInjection
import ScrollViewController
final class SearchController: UIViewController {
@KeyObject(.email, defaultValue: nil) var email: String?
@KeyObject(.phone, defaultValue: nil) var phone: String?
@KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool
@KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool
@Dependency private var hud: HUDType
@Dependency private var coordinator: SearchCoordinating
lazy private var tableController = SearchTableController(viewModel)
lazy private var screenView = SearchView {
let actionButton = CapsuleButton()
actionButton.set(
style: .seeThrough,
title: Localized.Ud.Placeholder.Drawer.action
)
let drawer = DrawerController(with: [
DrawerText(
font: Fonts.Mulish.bold.font(size: 26.0),
text: Localized.Ud.Placeholder.Drawer.title,
color: Asset.neutralActive.color,
alignment: .left,
spacingAfter: 19
),
DrawerLinkText(
text: Localized.Ud.Placeholder.Drawer.subtitle,
urlString: "https://links.xx.network/adrp",
spacingAfter: 37
),
DrawerStack(views: [
actionButton,
FlexibleSpace()
])
])
actionButton.publisher(for: .touchUpInside)
.receive(on: DispatchQueue.main)
.sink {
drawer.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.drawerCancellables.removeAll()
}
}.store(in: &self.drawerCancellables)
self.coordinator.toDrawer(drawer, from: self)
}
private let viewModel = SearchViewModel()
private var cancellables = Set<AnyCancellable>()
private var drawerCancellables = Set<AnyCancellable>()
override func loadView() {
view = screenView
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.didAppear()
}
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupTableView()
setupBindings()
setupFilterBindings()
}
private func setupTableView() {
addChild(tableController)
screenView.addSubview(tableController.view)
tableController.view.snp.makeConstraints {
$0.top.equalTo(screenView.stack.snp.bottom).offset(20)
$0.left.bottom.right.equalToSuperview()
}
tableController.didMove(toParent: self)
tableController.tableView.delegate = self
screenView.bringSubviewToFront(screenView.empty)
screenView.bringSubviewToFront(screenView.placeholder)
}
private func setupNavigationBar() {
navigationItem.backButtonTitle = " "
let titleLabel = UILabel()
titleLabel.text = Localized.Ud.title
titleLabel.textColor = Asset.neutralActive.color
titleLabel.font = Fonts.Mulish.semiBold.font(size: 18.0)
let backButton = UIButton.back()
backButton.addTarget(self, action: #selector(didTapBack), for: .touchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(
customView: UIStackView(arrangedSubviews: [backButton, titleLabel])
)
}
private func setupBindings() {
viewModel.successPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in presentSucessDrawerFor(contact: $0) }
.store(in: &cancellables)
viewModel.hudPublisher
.receive(on: DispatchQueue.main)
.sink { [hud] in hud.update(with: $0) }
.store(in: &cancellables)
viewModel.coverTrafficPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in presentCoverTrafficDrawer() }
.store(in: &cancellables)
viewModel
.itemsRelay
.removeDuplicates()
.map(\.count)
.receive(on: DispatchQueue.main)
.sink { [unowned self] in screenView.empty.isHidden = $0 > 0 }
.store(in: &cancellables)
viewModel.placeholderPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in screenView.placeholder.isHidden = !$0 }
.store(in: &cancellables)
viewModel.statePublisher
.map(\.country)
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
screenView.phoneInput.set(prefix: $0.prefixWithFlag)
screenView.phoneInput.update(placeholder: $0.example)
}
.store(in: &cancellables)
screenView.input
.textPublisher
.removeDuplicates()
.compactMap { $0 }
.sink { [unowned self] in viewModel.didInput($0) }
.store(in: &cancellables)
screenView.input
.returnPublisher
.sink { [unowned self] in viewModel.didTapSearch() }
.store(in: &cancellables)
screenView.phoneInput
.returnPublisher
.sink { [unowned self] in viewModel.didTapSearch() }
.store(in: &cancellables)
screenView
.phoneInput
.textPublisher
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [unowned self] in viewModel.didInputPhone($0) }
.store(in: &cancellables)
screenView
.phoneInput
.codePublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
coordinator.toCountries(from: self) {
self.viewModel.didChooseCountry($0)
}
}.store(in: &cancellables)
}
private func setupFilterBindings() {
screenView.username
.publisher(for: .touchUpInside)
.sink { [unowned self] _ in viewModel.didSelect(filter: .username) }
.store(in: &cancellables)
screenView.phone
.publisher(for: .touchUpInside)
.sink { [unowned self] _ in viewModel.didSelect(filter: .phone) }
.store(in: &cancellables)
screenView.email
.publisher(for: .touchUpInside)
.sink { [unowned self] _ in viewModel.didSelect(filter: .email) }
.store(in: &cancellables)
viewModel.statePublisher
.map(\.selectedFilter)
.removeDuplicates()
.sink { [unowned self] in screenView.alternateFieldsOver(filter: $0) }
.store(in: &cancellables)
viewModel.statePublisher
.map(\.selectedFilter)
.removeDuplicates()
.dropFirst()
.sink { [unowned self] in screenView.select(filter: $0) }
.store(in: &cancellables)
}
@objc private func didTapBack() {
navigationController?.popViewController(animated: true)
}
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
let contact = viewModel.itemsRelay.value[indexPath.row]
guard contact.authStatus == .stranger else {
coordinator.toContact(contact, from: self)
return
}
presentRequestDrawer(forContact: contact)
}
}
extension SearchController: UITableViewDelegate {}
// MARK: - Contact Request Drawer
extension SearchController {
private func presentRequestDrawer(forContact contact: Contact) {
var items: [DrawerItem] = []
let drawerTitle = DrawerText(
font: Fonts.Mulish.extraBold.font(size: 26.0),
text: Localized.Ud.RequestDrawer.title,
color: Asset.neutralDark.color,
spacingAfter: 20
)
var subtitleFragment = "Share your information with #\(contact.username ?? "")"
if let email = contact.email {
subtitleFragment.append(contentsOf: " (\(email))#")
} else if let phone = contact.phone {
subtitleFragment.append(contentsOf: " (\(Country.findFrom(phone).prefix) \(phone.dropLast(2)))#")
} else {
subtitleFragment.append(contentsOf: "#")
}
subtitleFragment.append(contentsOf: " so they know its you.")
let drawerSubtitle = DrawerText(
font: Fonts.Mulish.regular.font(size: 16.0),
text: subtitleFragment,
color: Asset.neutralDark.color,
spacingAfter: 31.5,
customAttributes: [
.font: Fonts.Mulish.regular.font(size: 16.0) as Any,
.foregroundColor: Asset.brandPrimary.color
]
)
items.append(contentsOf: [
drawerTitle,
drawerSubtitle
])
if let email = email {
let drawerEmail = DrawerSwitch(
title: Localized.Ud.RequestDrawer.email,
content: email,
spacingAfter: phone != nil ? 23 : 31,
isInitiallyOn: isSharingEmail
)
items.append(drawerEmail)
drawerEmail.isOnPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.isSharingEmail = $0 }
.store(in: &drawerCancellables)
}
if let phone = phone {
let drawerPhone = DrawerSwitch(
title: Localized.Ud.RequestDrawer.phone,
content: "\(Country.findFrom(phone).prefix) \(phone.dropLast(2))",
spacingAfter: 31,
isInitiallyOn: isSharingPhone
)
items.append(drawerPhone)
drawerPhone.isOnPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.isSharingPhone = $0 }
.store(in: &drawerCancellables)
}
let drawerSendButton = DrawerCapsuleButton(
model: .init(
title: Localized.Ud.RequestDrawer.send,
style: .brandColored
), spacingAfter: 5
)
let drawerCancelButton = DrawerCapsuleButton(
model: .init(
title: Localized.Ud.RequestDrawer.cancel,
style: .simplestColoredBrand
), spacingAfter: 5
)
items.append(contentsOf: [drawerSendButton, drawerCancelButton])
let drawer = DrawerController(with: items)
drawerSendButton.action
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
drawer.dismiss(animated: true) {
self.viewModel.didTapRequest(contact: contact)
}
}.store(in: &drawerCancellables)
drawerCancelButton.action
.receive(on: DispatchQueue.main)
.sink { drawer.dismiss(animated: true) }
.store(in: &drawerCancellables)
coordinator.toDrawer(drawer, from: self)
}
}
// MARK: - Cover Traffic Drawer
extension SearchController {
private func presentCoverTrafficDrawer() {
let enableButton = CapsuleButton()
enableButton.set(
style: .brandColored,
title: Localized.ChatList.Traffic.positive
)
let dismissButton = CapsuleButton()
dismissButton.set(
style: .seeThrough,
title: Localized.ChatList.Traffic.negative
)
let drawer = DrawerController(with: [
DrawerText(
font: Fonts.Mulish.bold.font(size: 26.0),
text: Localized.ChatList.Traffic.title,
color: Asset.neutralActive.color,
alignment: .left,
spacingAfter: 19
),
DrawerText(
font: Fonts.Mulish.regular.font(size: 16.0),
text: Localized.ChatList.Traffic.subtitle,
color: Asset.neutralBody.color,
alignment: .left,
lineHeightMultiple: 1.1,
spacingAfter: 39
),
DrawerStack(
axis: .horizontal,
spacing: 20,
distribution: .fillEqually,
views: [enableButton, dismissButton]
)
])
enableButton
.publisher(for: .touchUpInside)
.receive(on: DispatchQueue.main)
.sink {
drawer.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.drawerCancellables.removeAll()
self.viewModel.didEnableCoverTraffic()
}
}.store(in: &drawerCancellables)
dismissButton
.publisher(for: .touchUpInside)
.receive(on: DispatchQueue.main)
.sink {
drawer.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.drawerCancellables.removeAll()
}
}.store(in: &drawerCancellables)
coordinator.toDrawer(drawer, from: self)
}
}
extension SearchController {
private func presentSucessDrawerFor(contact: Contact) {
var items: [DrawerItem] = []
let drawerTitle = DrawerText(
font: Fonts.Mulish.extraBold.font(size: 26.0),
text: Localized.Ud.NicknameDrawer.title,
color: Asset.neutralDark.color,
spacingAfter: 20
)
let drawerSubtitle = DrawerText(
font: Fonts.Mulish.regular.font(size: 16.0),
text: Localized.Ud.NicknameDrawer.subtitle,
color: Asset.neutralDark.color,
spacingAfter: 20
)
items.append(contentsOf: [
drawerTitle,
drawerSubtitle
])
let drawerNicknameInput = DrawerInput(
placeholder: contact.username!,
validator: .init(
wrongIcon: .image(Asset.sharedError.image),
correctIcon: .image(Asset.sharedSuccess.image),
shouldAcceptPlaceholder: true
),
spacingAfter: 29
)
items.append(drawerNicknameInput)
let drawerSaveButton = DrawerCapsuleButton(
model: .init(
title: Localized.Ud.NicknameDrawer.save,
style: .brandColored
), spacingAfter: 5
)
items.append(drawerSaveButton)
let drawer = DrawerController(with: items)
var nickname: String?
var allowsSave = true
drawerNicknameInput.validationPublisher
.receive(on: DispatchQueue.main)
.sink { allowsSave = $0 }
.store(in: &drawerCancellables)
drawerNicknameInput.inputPublisher
.receive(on: DispatchQueue.main)
.sink {
guard !$0.isEmpty else {
nickname = contact.username
return
}
nickname = $0
}
.store(in: &drawerCancellables)
drawerSaveButton.action
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
guard allowsSave else { return }
drawer.dismiss(animated: true) {
self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact)
}
}
.store(in: &drawerCancellables)
coordinator.toNicknameDrawer(drawer, from: self)
}
}
import UIKit
final class SearchEmailController: UIViewController {
lazy private var screenView = SearchEmailView()
override func loadView() {
view = screenView
}
}
import UIKit
final class SearchPhoneController: UIViewController {
lazy private var screenView = SearchPhoneView()
override func loadView() {
view = screenView
}
}
import UIKit
final class SearchQRController: UIViewController {
lazy private var screenView = SearchQRView()
override func loadView() {
view = screenView
}
}
...@@ -4,14 +4,10 @@ import Combine ...@@ -4,14 +4,10 @@ import Combine
import XXModels import XXModels
final class SearchTableController: UITableViewController { final class SearchTableController: UITableViewController {
// MARK: Properties
private let viewModel: SearchViewModel private let viewModel: SearchViewModel
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private(set) var dataSource = [Contact]() private(set) var dataSource = [Contact]()
// MARK: Lifecycle
init(_ viewModel: SearchViewModel) { init(_ viewModel: SearchViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
super.init(style: .grouped) super.init(style: .grouped)
...@@ -21,21 +17,11 @@ final class SearchTableController: UITableViewController { ...@@ -21,21 +17,11 @@ final class SearchTableController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupTableView()
setupBindings()
}
// MARK: Private
private func setupTableView() {
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.register(SearchCell.self) tableView.register(SearchCell.self)
}
private func setupBindings() { viewModel.itemsRelay
viewModel
.itemsRelay
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [unowned self] in .sink { [unowned self] in
dataSource = $0 dataSource = $0
...@@ -43,18 +29,27 @@ final class SearchTableController: UITableViewController { ...@@ -43,18 +29,27 @@ final class SearchTableController: UITableViewController {
}.store(in: &cancellables) }.store(in: &cancellables)
} }
// MARK: UITableViewDataSource override func tableView(
_ tableView: UITableView,
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath
cellForRowAt indexPath: IndexPath) -> UITableViewCell { ) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self) let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self)
cell.title.text = dataSource[indexPath.row].username let username = dataSource[indexPath.row].username!
cell.subtitle.text = dataSource[indexPath.row].username
cell.avatar.setupProfile(title: dataSource[indexPath.row].username!, image: nil, size: .large) cell.setup(
title: username,
subtitle: username,
avatarTitle: username,
avatarImage: nil,
avatarSize: .large
)
return cell return cell
} }
override func tableView(_ tableView: UITableView, override func tableView(
numberOfRowsInSection section: Int) -> Int { dataSource.count } _: UITableView,
numberOfRowsInSection: Int
) -> Int { dataSource.count }
} }
import UIKit
final class SearchUsernameController: UIViewController {
lazy private var screenView = SearchUsernameView()
override func loadView() {
view = screenView
}
}
import UIKit
import Shared
final class FilterItemView: UIControl {
enum Style {
case selected
case unselected
}
private let title = UILabel()
private let image = UIImageView()
private var icon: UIImage?
var style: Style = .unselected {
didSet {
image.image = icon?.withRenderingMode(.alwaysTemplate)
switch style {
case .selected:
backgroundColor = Asset.brandDefault.color
image.tintColor = Asset.neutralWhite.color
title.textColor = Asset.neutralWhite.color
title.font = Fonts.Mulish.bold.font(size: 14.0)
layer.borderColor = Asset.brandDefault.color.cgColor
case .unselected:
image.tintColor = Asset.neutralActive.color
title.textColor = Asset.neutralActive.color
backgroundColor = Asset.neutralSecondary.color
title.font = Fonts.Mulish.regular.font(size: 14.0)
layer.borderColor = Asset.neutralLine.color.cgColor
}
}
}
init() {
super.init(frame: .zero)
layer.borderWidth = 1
layer.cornerRadius = 4
image.contentMode = .center
let stack = UIStackView()
stack.isUserInteractionEnabled = false
stack.spacing = 8
stack.addArrangedSubview(image)
stack.addArrangedSubview(title)
addSubview(stack)
stack.snp.makeConstraints { $0.center.equalToSuperview() }
snp.makeConstraints { $0.height.equalTo(40) }
}
required init?(coder: NSCoder) { nil }
func set(
title: String,
icon: UIImage?,
style: Style = .unselected
) {
self.icon = icon
self.style = style
self.title.text = title
}
}
...@@ -2,72 +2,81 @@ import UIKit ...@@ -2,72 +2,81 @@ import UIKit
import Shared import Shared
final class SearchCell: UITableViewCell { final class SearchCell: UITableViewCell {
// MARK: UI private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
let title = UILabel() private let separatorView = UIView()
let subtitle = UILabel() private let avatarView = AvatarView()
let separator = UIView()
let avatar = AvatarView()
// MARK: Lifecycle
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
required init?(coder: NSCoder) { nil }
override func prepareForReuse() {
super.prepareForReuse()
title.text = nil
}
// MARK: Private
private func setup() {
selectionStyle = .none selectionStyle = .none
backgroundColor = Asset.neutralWhite.color backgroundColor = Asset.neutralWhite.color
title.textColor = Asset.neutralActive.color titleLabel.textColor = Asset.neutralActive.color
subtitle.textColor = Asset.neutralDisabled.color subtitleLabel.textColor = Asset.neutralDisabled.color
separator.backgroundColor = Asset.neutralLine.color separatorView.backgroundColor = Asset.neutralLine.color
title.font = Fonts.Mulish.semiBold.font(size: 14.0) titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
subtitle.font = Fonts.Mulish.regular.font(size: 12.0) subtitleLabel.font = Fonts.Mulish.regular.font(size: 12.0)
contentView.addSubview(title) contentView.addSubview(titleLabel)
contentView.addSubview(avatar) contentView.addSubview(avatarView)
contentView.addSubview(subtitle) contentView.addSubview(subtitleLabel)
contentView.addSubview(separator) contentView.addSubview(separatorView)
setupConstraints() setupConstraints()
} }
required init?(coder: NSCoder) { nil }
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil
subtitleLabel.text = nil
avatarView.prepareForReuse()
}
func setup(
title: String,
subtitle: String,
avatarTitle: String,
avatarImage: Data?,
avatarSize: AvatarView.Size
) {
titleLabel.text = title
subtitleLabel.text = subtitle
avatarView.setupProfile(
title: avatarTitle,
image: avatarImage,
size: avatarSize
)
}
private func setupConstraints() { private func setupConstraints() {
title.snp.makeConstraints { make in titleLabel.snp.makeConstraints {
make.top.equalToSuperview().offset(10) $0.top.equalToSuperview().offset(10)
make.left.equalTo(avatar.snp.right).offset(16) $0.left.equalTo(avatarView.snp.right).offset(16)
make.right.lessThanOrEqualToSuperview().offset(-20) $0.right.lessThanOrEqualToSuperview().offset(-20)
} }
subtitle.snp.makeConstraints { make in subtitleLabel.snp.makeConstraints {
make.top.equalTo(title.snp.bottom).offset(3) $0.top.equalTo(titleLabel.snp.bottom).offset(3)
make.left.equalTo(title) $0.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-22) $0.bottom.equalToSuperview().offset(-22)
} }
avatar.snp.makeConstraints { make in avatarView.snp.makeConstraints {
make.left.equalToSuperview().offset(28) $0.left.equalToSuperview().offset(28)
make.width.height.equalTo(48) $0.width.height.equalTo(48)
make.bottom.equalToSuperview().offset(-16) $0.bottom.equalToSuperview().offset(-16)
} }
separator.snp.makeConstraints { make in separatorView.snp.makeConstraints {
make.height.equalTo(1) $0.height.equalTo(1)
make.left.equalToSuperview().offset(24) $0.left.equalToSuperview().offset(24)
make.right.equalToSuperview().offset(-24) $0.right.equalToSuperview().offset(-24)
make.bottom.equalToSuperview() $0.bottom.equalToSuperview()
} }
} }
} }
...@@ -12,17 +12,24 @@ final class SearchContainerView: UIView { ...@@ -12,17 +12,24 @@ final class SearchContainerView: UIView {
addSubview(segmentedControl) addSubview(segmentedControl)
addSubview(scrollView) addSubview(scrollView)
scrollView.snp.makeConstraints { setupConstraints()
$0.edges.equalToSuperview() }
}
required init?(coder: NSCoder) { nil }
private func setupConstraints() {
segmentedControl.snp.makeConstraints { segmentedControl.snp.makeConstraints {
$0.top.equalTo(safeAreaLayoutGuide).offset(10) $0.top.equalTo(safeAreaLayoutGuide).offset(10)
$0.left.equalToSuperview() $0.left.equalToSuperview()
$0.right.equalToSuperview() $0.right.equalToSuperview()
$0.height.equalTo(60) $0.height.equalTo(60)
} }
}
required init?(coder: NSCoder) { nil } scrollView.snp.makeConstraints {
$0.top.equalTo(segmentedControl.snp.bottom)
$0.left.equalToSuperview()
$0.right.equalToSuperview()
$0.bottom.equalToSuperview()
}
}
} }
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 }
}
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 }
}
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 }
}
...@@ -2,8 +2,10 @@ import UIKit ...@@ -2,8 +2,10 @@ import UIKit
import Shared import Shared
final class SearchSegmentedButton: UIControl { final class SearchSegmentedButton: UIControl {
let titleLabel = UILabel() private let titleLabel = UILabel()
let imageView = UIImageView() private let imageView = UIImageView()
private let highlightColor = Asset.brandPrimary.color
private let discreteColor = Asset.neutralDisabled.color
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
...@@ -16,6 +18,35 @@ final class SearchSegmentedButton: UIControl { ...@@ -16,6 +18,35 @@ final class SearchSegmentedButton: UIControl {
addSubview(titleLabel) addSubview(titleLabel)
addSubview(imageView) addSubview(imageView)
setupConstraints()
}
required init?(coder: NSCoder) { nil }
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 updateHighlighting(rate: CGFloat) {
let color = UIColor.fade(
from: discreteColor,
to: highlightColor,
pcent: rate
)
imageView.tintColor = color
titleLabel.textColor = color
}
private func setupConstraints() {
imageView.snp.makeConstraints { imageView.snp.makeConstraints {
$0.top.equalToSuperview().offset(7.5) $0.top.equalToSuperview().offset(7.5)
$0.centerX.equalToSuperview() $0.centerX.equalToSuperview()
...@@ -27,16 +58,4 @@ final class SearchSegmentedButton: UIControl { ...@@ -27,16 +58,4 @@ final class SearchSegmentedButton: UIControl {
$0.bottom.equalToSuperview().offset(-7.5) $0.bottom.equalToSuperview().offset(-7.5)
} }
} }
required init?(coder: NSCoder) { nil }
func setup(title: String, icon: UIImage) {
titleLabel.text = title
imageView.image = icon
}
func update(color: UIColor) {
imageView.tintColor = color
titleLabel.textColor = color
}
} }
import UIKit import UIKit
import Shared import Shared
import SnapKit import SnapKit
import Combine
final class SearchSegmentedControl: UIView { final class SearchSegmentedControl: UIView {
enum Item: Int {
case username = 0
case email
case phone
case qr
}
private let trackView = UIView() private let trackView = UIView()
private let stackView = UIStackView() private let stackView = UIStackView()
private var leftConstraint: Constraint?
private let trackIndicatorView = UIView() private let trackIndicatorView = UIView()
private(set) var leftConstraint: Constraint?
private(set) var usernameButton = SearchSegmentedButton() private(set) var usernameButton = SearchSegmentedButton()
private(set) var emailButton = SearchSegmentedButton() private(set) var emailButton = SearchSegmentedButton()
private(set) var phoneButton = SearchSegmentedButton() private(set) var phoneButton = SearchSegmentedButton()
private(set) var qrCodeButton = 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() { init() {
super.init(frame: .zero) super.init(frame: .zero)
trackView.backgroundColor = Asset.neutralLine.color trackView.backgroundColor = Asset.neutralLine.color
trackIndicatorView.backgroundColor = Asset.brandPrimary.color trackIndicatorView.backgroundColor = Asset.brandPrimary.color
qrCodeButton.titleLabel.text = Localized.Ud.Tab.qr usernameButton.setup(
emailButton.titleLabel.text = Localized.Ud.Tab.email title: Localized.Ud.Tab.username,
phoneButton.titleLabel.text = Localized.Ud.Tab.phone icon: Asset.searchTabUsername.image,
usernameButton.titleLabel.text = Localized.Ud.Tab.username iconColor: Asset.brandPrimary.color,
titleColor: Asset.brandPrimary.color
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
qrCodeButton.imageView.image = Asset.searchTabQr.image qrCodeButton.setup(title: Localized.Ud.Tab.qr, icon: Asset.searchTabQr.image)
emailButton.imageView.image = Asset.searchTabEmail.image emailButton.setup(title: Localized.Ud.Tab.email, icon: Asset.searchTabEmail.image)
phoneButton.imageView.image = Asset.searchTabPhone.image phoneButton.setup(title: Localized.Ud.Tab.phone, icon: Asset.searchTabPhone.image)
usernameButton.imageView.image = Asset.searchTabUsername.image
stackView.distribution = .fillEqually
stackView.addArrangedSubview(usernameButton) stackView.addArrangedSubview(usernameButton)
stackView.addArrangedSubview(emailButton) stackView.addArrangedSubview(emailButton)
stackView.addArrangedSubview(phoneButton) stackView.addArrangedSubview(phoneButton)
stackView.addArrangedSubview(qrCodeButton) stackView.addArrangedSubview(qrCodeButton)
stackView.distribution = .fillEqually
stackView.backgroundColor = Asset.neutralWhite.color stackView.backgroundColor = Asset.neutralWhite.color
addSubview(stackView) addSubview(stackView)
addSubview(trackView) addSubview(trackView)
trackView.addSubview(trackIndicatorView) 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 { stackView.snp.makeConstraints {
$0.edges.equalToSuperview() $0.edges.equalToSuperview()
} }
...@@ -66,55 +101,4 @@ final class SearchSegmentedControl: UIView { ...@@ -66,55 +101,4 @@ final class SearchSegmentedControl: UIView {
$0.bottom.equalToSuperview() $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
}
} }
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 }
}
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 }
}
import UIKit
import Shared
import InputField
final class SearchView: UIView {
private enum Constants {
static let phone = Localized.Ud.Tab.phone
static let email = Localized.Ud.Tab.email
static let username = Localized.Ud.Tab.username
}
let input = InputField()
let stack = UIStackView()
let filters = UIStackView()
let email = FilterItemView()
let phone = FilterItemView()
let empty = SearchEmptyView()
let phoneInput = InputField()
let username = FilterItemView()
lazy var placeholder = SearchPlaceholderView { self.didTapInfo() }
let didTapInfo: () -> Void
init(didTapInfo: @escaping () -> Void) {
self.didTapInfo = didTapInfo
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) { nil }
func alternateFieldsOver(filter: SelectedFilter) {
switch filter {
case .username, .email:
input.isHidden = false
phoneInput.isHidden = true
case .phone:
input.isHidden = true
phoneInput.isHidden = false
}
}
func select(filter: SelectedFilter) {
[username, email, phone].forEach { $0.style = .unselected }
switch filter {
case .username:
username.style = .selected
empty.set(filter: Constants.username.lowercased())
input.makeFirstResponder()
case .email:
email.style = .selected
empty.set(filter: Constants.email.lowercased())
input.makeFirstResponder()
case .phone:
phone.style = .selected
empty.set(filter: Constants.phone.lowercased())
phoneInput.makeFirstResponder()
}
}
// MARK: Private
private func setup() {
backgroundColor = Asset.neutralWhite.color
input.setup(
placeholder: Localized.Ud.title,
leftView: .image(Asset.lens.image.withTintColor(Asset.neutralDisabled.color)),
accessibility: Localized.Accessibility.Search.input,
allowsEmptySpace: false,
autocapitalization: .none,
returnKeyType: .search,
clearable: true
)
phoneInput.setup(
style: .phone,
placeholder: "1509192596",
rightView: .image(Asset.searchLens.image),
accessibility: Localized.Accessibility.Search.phoneInput,
keyboardType: .numberPad,
contentType: .telephoneNumber,
returnKeyType: .search,
toolbarButtonTitle: Localized.Shared.Search.placeholder,
codeAccessibility: Localized.Accessibility.Search.countryCode
)
email.set(title: Constants.email, icon: Asset.searchEmail.image)
phone.set(title: Constants.phone, icon: Asset.searchPhone.image)
username.set(title: Constants.username, icon: Asset.searchUsername.image, style: .selected)
email.accessibilityIdentifier = Localized.Accessibility.Search.email
phone.accessibilityIdentifier = Localized.Accessibility.Search.phone
username.accessibilityIdentifier = Localized.Accessibility.Search.username
filters.addArrangedSubview(username)
filters.addArrangedSubview(email)
filters.addArrangedSubview(phone)
filters.distribution = .fillEqually
filters.spacing = 20
stack.axis = .vertical
stack.addArrangedSubview(filters)
stack.addArrangedSubview(input)
stack.addArrangedSubview(phoneInput)
addSubview(stack)
addSubview(empty)
addSubview(placeholder)
stack.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14)
make.left.equalToSuperview().offset(17)
make.right.equalToSuperview().offset(-17)
}
placeholder.snp.makeConstraints { make in
make.top.equalTo(stack.snp.bottom)
make.left.bottom.right.equalToSuperview()
}
empty.snp.makeConstraints { make in
make.top.equalTo(stack.snp.bottom)
make.left.bottom.right.equalToSuperview()
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment