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 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
}
}
}
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
import XXModels
final class SearchTableController: UITableViewController {
// MARK: Properties
private let viewModel: SearchViewModel
private var cancellables = Set<AnyCancellable>()
private(set) var dataSource = [Contact]()
// MARK: Lifecycle
init(_ viewModel: SearchViewModel) {
self.viewModel = viewModel
super.init(style: .grouped)
......@@ -21,21 +17,11 @@ final class SearchTableController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupBindings()
}
// MARK: Private
private func setupTableView() {
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.register(SearchCell.self)
}
private func setupBindings() {
viewModel
.itemsRelay
viewModel.itemsRelay
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
dataSource = $0
......@@ -43,18 +29,27 @@ final class SearchTableController: UITableViewController {
}.store(in: &cancellables)
}
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self)
cell.title.text = dataSource[indexPath.row].username
cell.subtitle.text = dataSource[indexPath.row].username
cell.avatar.setupProfile(title: dataSource[indexPath.row].username!, image: nil, size: .large)
let username = dataSource[indexPath.row].username!
cell.setup(
title: username,
subtitle: username,
avatarTitle: username,
avatarImage: nil,
avatarSize: .large
)
return cell
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int { dataSource.count }
override func tableView(
_: 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
import Shared
final class SearchCell: UITableViewCell {
// MARK: UI
let title = UILabel()
let subtitle = UILabel()
let separator = UIView()
let avatar = AvatarView()
// MARK: Lifecycle
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let separatorView = UIView()
private let avatarView = AvatarView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
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
backgroundColor = Asset.neutralWhite.color
title.textColor = Asset.neutralActive.color
subtitle.textColor = Asset.neutralDisabled.color
separator.backgroundColor = Asset.neutralLine.color
titleLabel.textColor = Asset.neutralActive.color
subtitleLabel.textColor = Asset.neutralDisabled.color
separatorView.backgroundColor = Asset.neutralLine.color
title.font = Fonts.Mulish.semiBold.font(size: 14.0)
subtitle.font = Fonts.Mulish.regular.font(size: 12.0)
titleLabel.font = Fonts.Mulish.semiBold.font(size: 14.0)
subtitleLabel.font = Fonts.Mulish.regular.font(size: 12.0)
contentView.addSubview(title)
contentView.addSubview(avatar)
contentView.addSubview(subtitle)
contentView.addSubview(separator)
contentView.addSubview(titleLabel)
contentView.addSubview(avatarView)
contentView.addSubview(subtitleLabel)
contentView.addSubview(separatorView)
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() {
title.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.left.equalTo(avatar.snp.right).offset(16)
make.right.lessThanOrEqualToSuperview().offset(-20)
titleLabel.snp.makeConstraints {
$0.top.equalToSuperview().offset(10)
$0.left.equalTo(avatarView.snp.right).offset(16)
$0.right.lessThanOrEqualToSuperview().offset(-20)
}
subtitle.snp.makeConstraints { make in
make.top.equalTo(title.snp.bottom).offset(3)
make.left.equalTo(title)
make.bottom.equalToSuperview().offset(-22)
subtitleLabel.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(3)
$0.left.equalTo(titleLabel)
$0.bottom.equalToSuperview().offset(-22)
}
avatar.snp.makeConstraints { make in
make.left.equalToSuperview().offset(28)
make.width.height.equalTo(48)
make.bottom.equalToSuperview().offset(-16)
avatarView.snp.makeConstraints {
$0.left.equalToSuperview().offset(28)
$0.width.height.equalTo(48)
$0.bottom.equalToSuperview().offset(-16)
}
separator.snp.makeConstraints { make in
make.height.equalTo(1)
make.left.equalToSuperview().offset(24)
make.right.equalToSuperview().offset(-24)
make.bottom.equalToSuperview()
separatorView.snp.makeConstraints {
$0.height.equalTo(1)
$0.left.equalToSuperview().offset(24)
$0.right.equalToSuperview().offset(-24)
$0.bottom.equalToSuperview()
}
}
}
......@@ -12,17 +12,24 @@ final class SearchContainerView: UIView {
addSubview(segmentedControl)
addSubview(scrollView)
scrollView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
setupConstraints()
}
required init?(coder: NSCoder) { nil }
private func setupConstraints() {
segmentedControl.snp.makeConstraints {
$0.top.equalTo(safeAreaLayoutGuide).offset(10)
$0.left.equalToSuperview()
$0.right.equalToSuperview()
$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
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)
......@@ -16,6 +18,35 @@ final class SearchSegmentedButton: UIControl {
addSubview(titleLabel)
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 {
$0.top.equalToSuperview().offset(7.5)
$0.centerX.equalToSuperview()
......@@ -27,16 +58,4 @@ final class SearchSegmentedButton: UIControl {
$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 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
}
}
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