From c4965691a92252a305816df965580496044a00af Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Mon, 18 Jul 2022 22:50:09 -0300 Subject: [PATCH] Finished input search logic, missing phone ui component and qr --- .../SearchContainerController.swift | 22 +-- .../Controllers/SearchLeftController.swift | 13 +- .../Controllers/SearchRightController.swift | 140 ++---------------- .../Utils/SearchDiffableDataSource.swift | 23 +++ .../ViewModels/SearchLeftViewModel.swift | 9 +- .../ViewModels/SearchRightViewModel.swift | 112 ++++++++++++++ .../Views/SearchLeftEmptyView.swift | 25 ++++ .../Views/SearchLeftPlaceholderView.swift | 4 +- .../SearchFeature/Views/SearchLeftView.swift | 35 ++--- .../Views/SearchSegmentedControl.swift | 9 ++ Sources/Shared/AutoGenerated/Strings.swift | 30 ++-- .../Resources/en.lproj/Localizable.strings | 16 +- 12 files changed, 232 insertions(+), 206 deletions(-) create mode 100644 Sources/SearchFeature/Utils/SearchDiffableDataSource.swift create mode 100644 Sources/SearchFeature/ViewModels/SearchRightViewModel.swift create mode 100644 Sources/SearchFeature/Views/SearchLeftEmptyView.swift diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index efc85dcf..ea6c89d1 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -6,27 +6,6 @@ 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 var coordinator: SearchCoordinating @Dependency var statusBarController: StatusBarStyleControlling @@ -89,6 +68,7 @@ public final class SearchContainerController: UIViewController { screenView.scrollView.setContentOffset(point, animated: true) } else { screenView.scrollView.setContentOffset(.zero, animated: true) + leftController.viewModel.didSelectItem($0) } }.store(in: &cancellables) diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index 35014bc2..b172eb74 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -19,11 +19,11 @@ final class SearchLeftController: UIViewController { lazy private var screenView = SearchLeftView() - private let viewModel = SearchLeftViewModel() private var cancellables = Set<AnyCancellable>() + private var dataSource: SearchDiffableDataSource! + private(set) var viewModel = SearchLeftViewModel() private var drawerCancellables = Set<AnyCancellable>() private let adrpURLString = "https://links.xx.network/adrp" - private var dataSource: SearchTableViewDiffableDataSource! override func loadView() { view = screenView @@ -42,7 +42,7 @@ final class SearchLeftController: UIViewController { screenView.tableView.dataSource = dataSource screenView.tableView.delegate = self - dataSource = SearchTableViewDiffableDataSource( + dataSource = SearchDiffableDataSource( tableView: screenView.tableView ) { tableView, indexPath, item in let contact: Contact @@ -75,6 +75,13 @@ final class SearchLeftController: UIViewController { .sink { [hud] in hud.update(with: $0) } .store(in: &cancellables) + viewModel.statePublisher + .map(\.item) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [unowned self] in screenView.updateUIForItem(item: $0) } + .store(in: &cancellables) + viewModel.statePublisher .compactMap(\.snapshot) .receive(on: DispatchQueue.main) diff --git a/Sources/SearchFeature/Controllers/SearchRightController.swift b/Sources/SearchFeature/Controllers/SearchRightController.swift index b8468f11..7aa7dd6a 100644 --- a/Sources/SearchFeature/Controllers/SearchRightController.swift +++ b/Sources/SearchFeature/Controllers/SearchRightController.swift @@ -1,27 +1,10 @@ import UIKit -import Permissions import DependencyInjection -enum SearchQRStatus: Equatable { - case reading - case processing - case success - case failed(SearchQRError) -} - -enum SearchQRError: Equatable { - case requestOpened - case unknown(String) - case cameraPermission - case alreadyFriends(String) -} - final class SearchRightController: UIViewController { - @Dependency private var permissions: PermissionHandling - lazy private var screenView = SearchRightView() - private let camera = Camera() +// private let camera = Camera() private var status: SearchQRStatus? override func loadView() { @@ -54,111 +37,20 @@ final class SearchRightController: UIViewController { } private func startCamera() { - permissions.requestCamera { [weak self] granted in - guard let self = self else { return } - - if granted { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.camera.start() - } - } else { - DispatchQueue.main.async { - self.status = .failed(.cameraPermission) -// self.screenView.update(with: .failed(.cameraPermission)) - } - } - } - } -} - - -import Combine -import AVFoundation - -protocol CameraType { - func start() - func stop() - - var previewLayer: CALayer { get } - var dataPublisher: AnyPublisher<Data, Never> { get } -} - -final class Camera: NSObject, CameraType { - var dataPublisher: AnyPublisher<Data, Never> { - dataSubject - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - lazy var previewLayer: CALayer = { - let layer = AVCaptureVideoPreviewLayer(session: session) - layer.videoGravity = .resizeAspectFill - return layer - }() - - private let session = AVCaptureSession() - private let metadataOutput = AVCaptureMetadataOutput() - private let dataSubject = PassthroughSubject<Data, Never>() - - override init() { - super.init() - setupCameraDevice() - } - - func start() { - guard session.isRunning == false else { return } - session.startRunning() - } - - func stop() { - guard session.isRunning == true else { return } - session.stopRunning() - } - - private func setupCameraDevice() { - if let captureDevice = AVCaptureDevice.default(for: .video), - let input = try? AVCaptureDeviceInput(device: captureDevice) { - - if session.canAddInput(input) && session.canAddOutput(metadataOutput) { - session.addInput(input) - session.addOutput(metadataOutput) - } - - metadataOutput.setMetadataObjectsDelegate(self, queue: .main) - metadataOutput.metadataObjectTypes = [.qr] - } - } -} - -extension Camera: AVCaptureMetadataOutputObjectsDelegate { - func metadataOutput( - _ output: AVCaptureMetadataOutput, - didOutput metadataObjects: [AVMetadataObject], - from connection: AVCaptureConnection - ) { - guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, - let data = object.stringValue?.data(using: .nonLossyASCII), object.type == .qr else { return } - dataSubject.send(data) - } -} - -final class MockCamera: NSObject, CameraType { - private let dataSubject = PassthroughSubject<Data, Never>() - - func start() { - DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in - self?.dataSubject.send("###".data(using: .utf8)!) - } - } - - func stop() {} - - var previewLayer: CALayer { CALayer() } - - var dataPublisher: AnyPublisher<Data, Never> { - dataSubject - .receive(on: DispatchQueue.main) - .eraseToAnyPublisher() +// permissions.requestCamera { [weak self] granted in +// guard let self = self else { return } +// +// if granted { +// DispatchQueue.main.async { [weak self] in +// guard let self = self else { return } +// self.camera.start() +// } +// } else { +// DispatchQueue.main.async { +// self.status = .failed(.cameraPermission) +//// self.screenView.update(with: .failed(.cameraPermission)) +// } +// } +// } } } diff --git a/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift new file mode 100644 index 00000000..4fef0dfa --- /dev/null +++ b/Sources/SearchFeature/Utils/SearchDiffableDataSource.swift @@ -0,0 +1,23 @@ +import UIKit +import XXModels + +enum SearchSection { + case stranger + case connections +} + +enum SearchItem: Equatable, Hashable { + case stranger(Contact) + case connection(Contact) +} + +class SearchDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .stranger: + return "" + case .connections: + return "CONNECTIONS" + } + } +} diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index effe59c7..1607e4ec 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -10,6 +10,7 @@ typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchIte struct SearchLeftViewState { var input = "" var snapshot: SearchSnapshot? + var item: SearchSegmentedControl.Item = .username } final class SearchLeftViewModel { @@ -35,11 +36,17 @@ final class SearchLeftViewModel { stateSubject.value.input = string } + func didSelectItem(_ item: SearchSegmentedControl.Item) { + stateSubject.value.item = item + } + func didStartSearching() { hudSubject.send(.on(nil)) + let prefix = stateSubject.value.item.written.first!.uppercased() + do { - try session.search(fact: "U\(stateSubject.value.input)") { [weak self] in + try session.search(fact: "\(prefix)\(stateSubject.value.input)") { [weak self] in guard let self = self else { return } switch $0 { diff --git a/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift new file mode 100644 index 00000000..663fb5db --- /dev/null +++ b/Sources/SearchFeature/ViewModels/SearchRightViewModel.swift @@ -0,0 +1,112 @@ +import Permissions +import DependencyInjection + +enum SearchQRStatus: Equatable { + case reading + case processing + case success + case failed(SearchQRError) +} + +enum SearchQRError: Equatable { + case requestOpened + case unknown(String) + case cameraPermission + case alreadyFriends(String) +} + +final class SearchRightViewModel { + @Dependency private var permissions: PermissionHandling +} + +// +// +//import Combine +//import AVFoundation +// +//protocol CameraType { +// func start() +// func stop() +// +// var previewLayer: CALayer { get } +// var dataPublisher: AnyPublisher<Data, Never> { get } +//} +// +//final class Camera: NSObject, CameraType { +// var dataPublisher: AnyPublisher<Data, Never> { +// dataSubject +// .receive(on: DispatchQueue.main) +// .eraseToAnyPublisher() +// } +// +// lazy var previewLayer: CALayer = { +// let layer = AVCaptureVideoPreviewLayer(session: session) +// layer.videoGravity = .resizeAspectFill +// return layer +// }() +// +// private let session = AVCaptureSession() +// private let metadataOutput = AVCaptureMetadataOutput() +// private let dataSubject = PassthroughSubject<Data, Never>() +// +// override init() { +// super.init() +// setupCameraDevice() +// } +// +// func start() { +// guard session.isRunning == false else { return } +// session.startRunning() +// } +// +// func stop() { +// guard session.isRunning == true else { return } +// session.stopRunning() +// } +// +// private func setupCameraDevice() { +// if let captureDevice = AVCaptureDevice.default(for: .video), +// let input = try? AVCaptureDeviceInput(device: captureDevice) { +// +// if session.canAddInput(input) && session.canAddOutput(metadataOutput) { +// session.addInput(input) +// session.addOutput(metadataOutput) +// } +// +// metadataOutput.setMetadataObjectsDelegate(self, queue: .main) +// metadataOutput.metadataObjectTypes = [.qr] +// } +// } +//} +// +//extension Camera: AVCaptureMetadataOutputObjectsDelegate { +// func metadataOutput( +// _ output: AVCaptureMetadataOutput, +// didOutput metadataObjects: [AVMetadataObject], +// from connection: AVCaptureConnection +// ) { +// guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, +// let data = object.stringValue?.data(using: .nonLossyASCII), object.type == .qr else { return } +// dataSubject.send(data) +// } +//} +// +//final class MockCamera: NSObject, CameraType { +// private let dataSubject = PassthroughSubject<Data, Never>() +// +// func start() { +// DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in +// self?.dataSubject.send("###".data(using: .utf8)!) +// } +// } +// +// func stop() {} +// +// var previewLayer: CALayer { CALayer() } +// +// var dataPublisher: AnyPublisher<Data, Never> { +// dataSubject +// .receive(on: DispatchQueue.main) +// .eraseToAnyPublisher() +// } +//} diff --git a/Sources/SearchFeature/Views/SearchLeftEmptyView.swift b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift new file mode 100644 index 00000000..f52174b7 --- /dev/null +++ b/Sources/SearchFeature/Views/SearchLeftEmptyView.swift @@ -0,0 +1,25 @@ +import UIKit +import Shared + +final class SearchLeftEmptyView: UIView { + let titleLabel = UILabel() + + init() { + super.init(frame: .zero) + + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + titleLabel.font = Fonts.Mulish.regular.font(size: 15.0) + titleLabel.textColor = Asset.neutralSecondaryAlternative.color + + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.center.equalToSuperview() + $0.left.equalToSuperview().offset(20) + $0.right.equalToSuperview().offset(-20) + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift index 5f6264be..b0fe2d60 100644 --- a/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift +++ b/Sources/SearchFeature/Views/SearchLeftPlaceholderView.swift @@ -16,7 +16,7 @@ final class SearchLeftPlaceholderView: UIView { super.init(frame: .zero) let attrString = NSMutableAttributedString( - string: Localized.Ud.Search.Username.Placeholder.title, + string: Localized.Ud.Search.Placeholder.title, attributes: [ .foregroundColor: Asset.neutralDark.color, .font: Fonts.Mulish.bold.font(size: 32.0) @@ -36,7 +36,7 @@ final class SearchLeftPlaceholderView: UIView { paragraph.lineHeightMultiple = 1.3 subtitleWithInfo.setup( - text: Localized.Ud.Search.Username.Placeholder.subtitle, + text: Localized.Ud.Search.Placeholder.subtitle, attributes: [ .paragraphStyle: paragraph, .foregroundColor: Asset.neutralBody.color, diff --git a/Sources/SearchFeature/Views/SearchLeftView.swift b/Sources/SearchFeature/Views/SearchLeftView.swift index f9df64ab..d0025965 100644 --- a/Sources/SearchFeature/Views/SearchLeftView.swift +++ b/Sources/SearchFeature/Views/SearchLeftView.swift @@ -4,36 +4,11 @@ import Shared final class SearchLeftView: UIView { let tableView = UITableView() let inputField = SearchComponent() - let emptyView: UIView = { - let view = UIView() - let label = UILabel() - - label.numberOfLines = 0 - label.textAlignment = .center - label.font = Fonts.Mulish.regular.font(size: 15.0) - label.text = Localized.Ud.Search.Username.Empty.title - label.textColor = Asset.neutralSecondaryAlternative.color - - view.addSubview(label) - label.snp.makeConstraints { - $0.center.equalToSuperview() - $0.left.equalToSuperview().offset(20) - $0.right.equalToSuperview().offset(-20) - } - - return view - }() - + let emptyView = SearchLeftEmptyView() let placeholderView = SearchLeftPlaceholderView() init() { super.init(frame: .zero) - - inputField.set( - placeholder: Localized.Ud.Search.Username.input, - imageAtRight: nil - ) - emptyView.isHidden = true addSubview(tableView) @@ -46,6 +21,14 @@ final class SearchLeftView: UIView { required init?(coder: NSCoder) { nil } + func updateUIForItem(item: SearchSegmentedControl.Item) { + let emptyTitle = Localized.Ud.Search.empty(item.written) + emptyView.titleLabel.text = emptyTitle + + let inputFieldTitle = Localized.Ud.Search.input(item.written) + inputField.set(placeholder: inputFieldTitle, imageAtRight: nil) + } + private func setupConstraints() { inputField.snp.makeConstraints { $0.top.equalToSuperview().offset(20) diff --git a/Sources/SearchFeature/Views/SearchSegmentedControl.swift b/Sources/SearchFeature/Views/SearchSegmentedControl.swift index 05eabb36..61413603 100644 --- a/Sources/SearchFeature/Views/SearchSegmentedControl.swift +++ b/Sources/SearchFeature/Views/SearchSegmentedControl.swift @@ -9,6 +9,15 @@ final class SearchSegmentedControl: UIView { case email case phone case qr + + var written: String { + switch self { + case .qr: return "qr" + case .email: return "email" + case .phone: return "phone number" + case .username: return "username" + } + } } private let trackView = UIView() diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index ada5805c..6e3ada55 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -1238,27 +1238,19 @@ public enum Localized { public static let title = Localized.tr("Localizable", "ud.requestDrawer.title") } public enum Search { - public enum Email { - /// Search by email - public static let input = Localized.tr("Localizable", "ud.search.email.input") + /// There are no users with that %@. + public static func empty(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.empty", String(describing: p1)) } - public enum Phone { - /// Search by phone number - public static let input = Localized.tr("Localizable", "ud.search.phone.input") + /// Search by %@ + public static func input(_ p1: Any) -> String { + return Localized.tr("Localizable", "ud.search.input", String(describing: p1)) } - public enum Username { - /// Search by username - public static let input = Localized.tr("Localizable", "ud.search.username.input") - public enum Empty { - /// There are no users with that username - public static let title = Localized.tr("Localizable", "ud.search.username.empty.title") - } - public enum Placeholder { - /// Your searches are anonymous. Search information is never linked to your account or personally identifiable. - public static let subtitle = Localized.tr("Localizable", "ud.search.username.placeholder.subtitle") - /// Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel. - public static let title = Localized.tr("Localizable", "ud.search.username.placeholder.title") - } + public enum Placeholder { + /// Your searches are anonymous. Search information is never linked to your account or personally identifiable. + public static let subtitle = Localized.tr("Localizable", "ud.search.placeholder.subtitle") + /// Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel. + public static let title = Localized.tr("Localizable", "ud.search.placeholder.title") } } public enum Tab { diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 0668a396..c50b6c12 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -982,19 +982,15 @@ = "Edit your new contact’s nickname so you know who they are."; "ud.nicknameDrawer.save" = "Save"; -"ud.search.username.input" -= "Search by username"; -"ud.search.email.input" -= "Search by email"; -"ud.search.phone.input" -= "Search by phone number"; -"ud.search.username.empty.title" -= "There are no users with that username"; +"ud.search.input" += "Search by %@"; +"ud.search.empty" += "There are no users with that %@."; -"ud.search.username.placeholder.title" +"ud.search.placeholder.title" = "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel."; -"ud.search.username.placeholder.subtitle" +"ud.search.placeholder.subtitle" = "Your searches are anonymous. Search information is never linked to your account or personally identifiable."; // LaunchFeature -- GitLab