From 00c19ae1422d76b3b1ce6cfb115f4017eacfb1bb Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Mon, 18 Jul 2022 00:52:04 -0300 Subject: [PATCH] Almost done w/ username searching --- .../SearchContainerController.swift | 104 ++++++- .../Controllers/SearchQRController.swift | 2 +- .../SearchUsernameController.swift | 286 +++++++++++++++++- .../ViewModels/SearchContainerViewModel.swift | 58 ++++ .../ViewModels/SearchUsernameViewModel.swift | 96 ++++++ .../ViewModels/SearchViewModel.swift | 49 +-- .../Views/SearchUsernameView.swift | 9 + Sources/Shared/Views/SearchComponent.swift | 17 +- 8 files changed, 568 insertions(+), 53 deletions(-) create mode 100644 Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift create mode 100644 Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index 1baedc82..788d47de 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -2,17 +2,43 @@ import UIKit import Theme import Shared import Combine +import XXModels +import DrawerFeature import DependencyInjection +enum SearchSection { + case stranger + case connections +} + +enum SearchItem: Equatable, Hashable { + case stranger(Contact) + case connection(Contact) +} + +class SearchTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .stranger: + return "" + case .connections: + return "CONNECTIONS" + } + } +} + public final class SearchContainerController: UIViewController { - @Dependency private var statusBarController: StatusBarStyleControlling + @Dependency var coordinator: SearchCoordinating + @Dependency var statusBarController: StatusBarStyleControlling lazy private var screenView = SearchContainerView() - private var cancellables = Set<AnyCancellable>() private let qrController = SearchQRController() + private var cancellables = Set<AnyCancellable>() + private let viewModel = SearchContainerViewModel() private let emailController = SearchEmailController() private let phoneController = SearchPhoneController() + private var drawerCancellables = Set<AnyCancellable>() private let usernameController = SearchUsernameController() public override func loadView() { @@ -30,6 +56,11 @@ public final class SearchContainerController: UIViewController { ) } + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewModel.didAppear() + } + public override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() @@ -61,6 +92,12 @@ public final class SearchContainerController: UIViewController { let point: CGPoint = CGPoint(x: screenView.frame.width * page, y: 0.0) screenView.scrollView.setContentOffset(point, animated: true) }.store(in: &cancellables) + + viewModel.coverTrafficPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in presentCoverTrafficDrawer() } + .store(in: &cancellables) + } @objc private func didTapBack() { @@ -156,3 +193,66 @@ extension SearchContainerController: UIScrollViewDelegate { } } } + +extension SearchContainerController { + 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) + } +} diff --git a/Sources/SearchFeature/Controllers/SearchQRController.swift b/Sources/SearchFeature/Controllers/SearchQRController.swift index 49718728..b6941edd 100644 --- a/Sources/SearchFeature/Controllers/SearchQRController.swift +++ b/Sources/SearchFeature/Controllers/SearchQRController.swift @@ -65,7 +65,7 @@ final class SearchQRController: UIViewController { } else { DispatchQueue.main.async { self.status = .failed(.cameraPermission) - self.screenView.update(with: .failed(.cameraPermission)) +// self.screenView.update(with: .failed(.cameraPermission)) } } } diff --git a/Sources/SearchFeature/Controllers/SearchUsernameController.swift b/Sources/SearchFeature/Controllers/SearchUsernameController.swift index 563cc86f..33da386f 100644 --- a/Sources/SearchFeature/Controllers/SearchUsernameController.swift +++ b/Sources/SearchFeature/Controllers/SearchUsernameController.swift @@ -1,16 +1,29 @@ +import HUD import UIKit import Shared import Combine +import XXModels +import Defaults +import Countries import DrawerFeature import DependencyInjection final class SearchUsernameController: UIViewController { + @Dependency private var hud: HUDType @Dependency private var coordinator: SearchCoordinating + @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 + lazy private var screenView = SearchUsernameView() private var cancellables = Set<AnyCancellable>() + private let viewModel = SearchUsernameViewModel() private var drawerCancellables = Set<AnyCancellable>() + private let adrpURLString = "https://links.xx.network/adrp" + private var dataSource: SearchTableViewDiffableDataSource! override func loadView() { view = screenView @@ -18,15 +31,82 @@ final class SearchUsernameController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + setupTableView() setupBindings() } + private func setupTableView() { + screenView.tableView.separatorStyle = .none + screenView.tableView.tableFooterView = UIView() + screenView.tableView.register(SmallAvatarAndTitleCell.self) + screenView.tableView.dataSource = dataSource + screenView.tableView.delegate = self + + dataSource = SearchTableViewDiffableDataSource( + tableView: screenView.tableView + ) { tableView, indexPath, item in + let contact: Contact + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self) + + switch item { + case .stranger(let stranger): + contact = stranger + case .connection(let connection): + contact = connection + } + + let title = (contact.nickname ?? contact.username) ?? "" + cell.titleLabel.text = title + cell.avatarView.setupProfile(title: title, image: contact.photo, size: .medium) + return cell + } + } + private func setupBindings() { + viewModel.hudPublisher + .receive(on: DispatchQueue.main) + .sink { [hud] in hud.update(with: $0) } + .store(in: &cancellables) + + viewModel.statePublisher + .compactMap(\.snapshot) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.placeholderView.isHidden = true + dataSource.apply($0, animatingDifferences: false) + }.store(in: &cancellables) + screenView.placeholderView .infoPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] in presentSearchDisclaimer() } .store(in: &cancellables) + + screenView.inputField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterInput($0) } + .store(in: &cancellables) + + screenView.inputField + .returnPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in viewModel.didStartSearching() } + .store(in: &cancellables) + + screenView.inputField + .isEditingPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] isEditing in + UIView.animate(withDuration: 0.25) { + self.screenView.placeholderView.alpha = isEditing ? 0.1 : 1.0 + } + }.store(in: &cancellables) + + viewModel.successPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in presentSucessDrawerFor(contact: $0) } + .store(in: &cancellables) } private func presentSearchDisclaimer() { @@ -46,7 +126,7 @@ final class SearchUsernameController: UIViewController { ), DrawerLinkText( text: Localized.Ud.Placeholder.Drawer.subtitle, - urlString: "https://links.xx.network/adrp", + urlString: adrpURLString, spacingAfter: 37 ), DrawerStack(views: [ @@ -66,4 +146,208 @@ final class SearchUsernameController: UIViewController { coordinator.toDrawer(drawer, from: self) } + + 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) + } + + 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) + } + +} + +extension SearchUsernameController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let item = dataSource.itemIdentifier(for: indexPath) { + switch item { + case .stranger(let contact): + didTap(contact: contact) + case .connection(let contact): + didTap(contact: contact) + } + } + } + + private func didTap(contact: Contact) { + guard contact.authStatus == .stranger else { + coordinator.toContact(contact, from: self) + return + } + + presentRequestDrawer(forContact: contact) + } } diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift new file mode 100644 index 00000000..e308f6a5 --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -0,0 +1,58 @@ +import UIKit +import Combine +import Defaults +import Integration +import PushFeature +import DependencyInjection + +final class SearchContainerViewModel { + @Dependency var session: SessionType + @Dependency var pushHandler: PushHandling + + @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled + @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications + @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic + + var coverTrafficPublisher: AnyPublisher<Void, Never> { + coverTrafficSubject.eraseToAnyPublisher() + } + + private let coverTrafficSubject = PassthroughSubject<Void, Never>() + + func didAppear() { + verifyCoverTraffic() + verifyNotifications() + } + + func didEnableCoverTraffic() { + isCoverTrafficEnabled = true + session.setDummyTraffic(status: true) + } + + private func verifyCoverTraffic() { + guard offeredCoverTraffic == false else { return } + offeredCoverTraffic = true + coverTrafficSubject.send() + } + + private func verifyNotifications() { + guard pushNotifications == false else { return } + + pushHandler.requestAuthorization { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let granted): + if granted { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + + self.pushNotifications = granted + case .failure: + self.pushNotifications = false + } + } + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift b/Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift new file mode 100644 index 00000000..f64543be --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchUsernameViewModel.swift @@ -0,0 +1,96 @@ +import HUD +import UIKit +import Combine +import XXModels +import Integration +import DependencyInjection + +typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> + +struct SearchUsernameViewState { + var input = "" + var snapshot: SearchSnapshot? +} + +final class SearchUsernameViewModel { + @Dependency var session: SessionType + + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } + + var successPublisher: AnyPublisher<Contact, Never> { + successSubject.eraseToAnyPublisher() + } + + var statePublisher: AnyPublisher<SearchUsernameViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + private let successSubject = PassthroughSubject<Contact, Never>() + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) + private let stateSubject = CurrentValueSubject<SearchUsernameViewState, Never>(.init()) + + func didEnterInput(_ string: String) { + stateSubject.value.input = string + } + + func didStartSearching() { + hudSubject.send(.on(nil)) + + do { + try session.search(fact: "U\(stateSubject.value.input)") { [weak self] in + guard let self = self else { return } + + switch $0 { + case .success(let contact): + self.hudSubject.send(.none) + self.appendToLocalSearch(contact) + + case .failure(let error): + self.appendToLocalSearch(nil) + self.hudSubject.send(.error(.init(with: error))) + } + } + } catch { + hudSubject.send(.error(.init(with: error))) + } + } + + func didTapRequest(contact: Contact) { + hudSubject.send(.on(nil)) + var contact = contact + contact.nickname = contact.username + + do { + try self.session.add(contact) + hudSubject.send(.none) + successSubject.send(contact) + } catch { + hudSubject.send(.error(.init(with: error))) + } + } + + func didSet(nickname: String, for contact: Contact) { + if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first { + contact.nickname = nickname + _ = try? session.dbManager.saveContact(contact) + } + } + + private func appendToLocalSearch(_ contact: Contact?) { + var snapshot = SearchSnapshot() + + if let contact = contact { + snapshot.appendSections([.stranger]) + snapshot.appendItems([.stranger(contact)], toSection: .stranger) + } + + if let locals = try? session.dbManager.fetchContacts(Contact.Query(username: stateSubject.value.input)), locals.count > 0 { + snapshot.appendSections([.connections]) + snapshot.appendItems(locals.map(SearchItem.connection), toSection: .connections) + } + + stateSubject.value.snapshot = snapshot + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift index 5dae23c8..e59b7a85 100644 --- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchViewModel.swift @@ -36,12 +36,9 @@ struct SearchViewState: Equatable { } final class SearchViewModel { - @KeyObject(.dummyTrafficOn, defaultValue: false) var isCoverTrafficEnabled: Bool - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic: Bool @Dependency private var session: SessionType - @Dependency private var pushHandler: PushHandling + var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() @@ -51,10 +48,6 @@ final class SearchViewModel { placeholderSubject.eraseToAnyPublisher() } - var coverTrafficPublisher: AnyPublisher<Void, Never> { - coverTrafficSubject.eraseToAnyPublisher() - } - var statePublisher: AnyPublisher<SearchViewState, Never> { stateSubject.eraseToAnyPublisher() } @@ -68,15 +61,12 @@ final class SearchViewModel { let itemsRelay = CurrentValueSubject<[Contact], Never>([]) private let successSubject = PassthroughSubject<Contact, Never>() - private let coverTrafficSubject = PassthroughSubject<Void, Never>() + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) private let placeholderSubject = CurrentValueSubject<Bool, Never>(true) private let stateSubject = CurrentValueSubject<SearchViewState, Never>(.init()) - func didAppear() { - verifyCoverTraffic() - verifyNotifications() - } + func didSelect(filter: SelectedFilter) { stateSubject.value.selectedFilter = filter @@ -94,10 +84,6 @@ final class SearchViewModel { stateSubject.value.country = country } - func didEnableCoverTraffic() { - isCoverTrafficEnabled = true - session.setDummyTraffic(status: true) - } func didTapSearch() { hudSubject.send(.on(nil)) @@ -132,35 +118,6 @@ final class SearchViewModel { } } - private func verifyCoverTraffic() { - guard offeredCoverTraffic == false else { - return - } - - offeredCoverTraffic = true - coverTrafficSubject.send() - } - - private func verifyNotifications() { - guard pushNotifications == false else { return } - - pushHandler.requestAuthorization { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let granted): - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - - self.pushNotifications = granted - case .failure: - self.pushNotifications = false - } - } - } func didSet(nickname: String, for contact: Contact) { if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first { diff --git a/Sources/SearchFeature/Views/SearchUsernameView.swift b/Sources/SearchFeature/Views/SearchUsernameView.swift index e6de2ce0..1783ec3c 100644 --- a/Sources/SearchFeature/Views/SearchUsernameView.swift +++ b/Sources/SearchFeature/Views/SearchUsernameView.swift @@ -2,6 +2,7 @@ import UIKit import Shared final class SearchUsernameView: UIView { + let tableView = UITableView() let inputField = SearchComponent() let placeholderView = SearchUsernamePlaceholderView() @@ -13,6 +14,7 @@ final class SearchUsernameView: UIView { imageAtRight: nil ) + addSubview(tableView) addSubview(inputField) addSubview(placeholderView) @@ -28,6 +30,13 @@ final class SearchUsernameView: UIView { $0.right.equalToSuperview().offset(-20) } + tableView.snp.makeConstraints { + $0.top.equalTo(inputField.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() + } + placeholderView.snp.makeConstraints { $0.top.equalTo(inputField.snp.bottom) $0.left.equalToSuperview().offset(32.5) diff --git a/Sources/Shared/Views/SearchComponent.swift b/Sources/Shared/Views/SearchComponent.swift index 2e87cfbf..a12c0946 100644 --- a/Sources/Shared/Views/SearchComponent.swift +++ b/Sources/Shared/Views/SearchComponent.swift @@ -15,6 +15,10 @@ public final class SearchComponent: UIView { textSubject.eraseToAnyPublisher() } + public var returnPublisher: AnyPublisher<Void, Never> { + returnSubject.eraseToAnyPublisher() + } + private var rightImage = Asset.sharedScan.image { didSet { rightButton.setImage(rightImage, for: .normal) @@ -26,9 +30,10 @@ public final class SearchComponent: UIView { } private var cancellables = Set<AnyCancellable>() - private var rightSubject = PassthroughSubject<Void, Never>() - private var textSubject = PassthroughSubject<String, Never>() - private var isEditingSubject = CurrentValueSubject<Bool, Never>(false) + private let rightSubject = PassthroughSubject<Void, Never>() + private let textSubject = PassthroughSubject<String, Never>() + private let returnSubject = PassthroughSubject<Void, Never>() + private let isEditingSubject = CurrentValueSubject<Bool, Never>(false) public init() { super.init(frame: .zero) @@ -169,6 +174,12 @@ public final class SearchComponent: UIView { isEditingSubject.send(true) } + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + inputField.resignFirstResponder() + returnSubject.send(()) + return true + } + public func textFieldDidEndEditing(_ textField: UITextField) { rightButton.setImage(rightImage, for: .normal) isEditingSubject.send(false) -- GitLab