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

Finished qr part of search 2.0

parent 49db701e
No related branches found
No related tags found
2 merge requests!54Releasing 1.1.4,!50Search UI 2.0
......@@ -137,6 +137,8 @@ struct DependencyRegistrator {
container.register(
SearchCoordinator(
contactsFactory: ContactListController.init,
requestsFactory: RequestsContainerController.init,
contactFactory: ContactController.init(_:),
countriesFactory: CountryListController.init(_:)
) as SearchCoordinating)
......
......@@ -134,6 +134,12 @@ public final class Session: SessionType {
email = params.email
}
print(report.parameters)
guard username!.isEmpty == false else {
fatalError("Trying to restore an account that has no username")
}
try continueInitialization()
if !report.contactIds.isEmpty {
......
import Combine
import AVFoundation
final class CameraController: NSObject {
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]
}
}
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)
}
}
extension CameraController: AVCaptureMetadataOutputObjectsDelegate {}
import UIKit
import Combine
import DependencyInjection
final class SearchRightController: UIViewController {
@Dependency var coordinator: SearchCoordinating
lazy private var screenView = SearchRightView()
// private let camera = Camera()
private var status: SearchQRStatus?
private let viewModel = SearchRightViewModel()
private var cancellables = Set<AnyCancellable>()
private let cameraController = CameraController()
override func loadView() {
view = screenView
......@@ -13,44 +17,70 @@ final class SearchRightController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
///screenView.layer.insertSublayer(camera.previewLayer, at: 0)
///setupBindings()
screenView.layer.insertSublayer(cameraController.previewLayer, at: 0)
setupBindings()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
///camera.previewLayer.frame = screenView.bounds
cameraController.previewLayer.frame = screenView.bounds
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
///viewModel.resetScanner()
///startCamera()
viewModel.viewDidAppear()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// backgroundScheduler.schedule { [weak self] in
// guard let self = self else { return }
// self.camera.stop()
// }
viewModel.viewWillDisappear()
}
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))
// }
// }
// }
private func setupBindings() {
cameraController
.dataPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in viewModel.didScan(data: $0) }
.store(in: &cancellables)
viewModel.cameraSemaphorePublisher
.removeDuplicates()
.receive(on: DispatchQueue.global())
.sink { [unowned self] setOn in
if setOn {
cameraController.start()
} else {
cameraController.stop()
}
}.store(in: &cancellables)
viewModel.foundPublisher
.receive(on: DispatchQueue.main)
.delay(for: 1, scheduler: DispatchQueue.main)
.sink { [unowned self] in coordinator.toContact($0, from: self) }
.store(in: &cancellables)
viewModel.statusPublisher
.receive(on: DispatchQueue.main)
.removeDuplicates()
.sink { [unowned self] in screenView.update(status: $0) }
.store(in: &cancellables)
screenView.actionButton
.publisher(for: .touchUpInside)
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
switch viewModel.statusSubject.value {
case .failed(.cameraPermission):
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:])
case .failed(.requestOpened):
coordinator.toRequests(from: self)
case .failed(.alreadyFriends):
coordinator.toContacts(from: self)
default:
break
}
}.store(in: &cancellables)
}
}
......@@ -6,6 +6,8 @@ import Presentation
import ScrollViewController
public protocol SearchCoordinating {
func toRequests(from: UIViewController)
func toContacts(from: UIViewController)
func toContact(_: Contact, from: UIViewController)
func toDrawer(_: UIViewController, from: UIViewController)
func toNicknameDrawer(_: UIViewController, from: UIViewController)
......@@ -15,21 +17,38 @@ public protocol SearchCoordinating {
public struct SearchCoordinator {
var pushPresenter: Presenting = PushPresenter()
var bottomPresenter: Presenting = BottomPresenter()
var replacePresenter: Presenting = ReplacePresenter()
var fullscreenPresenter: Presenting = FullscreenPresenter()
var contactsFactory: () -> UIViewController
var requestsFactory: () -> UIViewController
var contactFactory: (Contact) -> UIViewController
var countriesFactory: (@escaping (Country) -> Void) -> UIViewController
public init(
contactsFactory: @escaping () -> UIViewController,
requestsFactory: @escaping () -> UIViewController,
contactFactory: @escaping (Contact) -> UIViewController,
countriesFactory: @escaping (@escaping (Country) -> Void) -> UIViewController
) {
self.contactFactory = contactFactory
self.contactsFactory = contactsFactory
self.requestsFactory = requestsFactory
self.countriesFactory = countriesFactory
}
}
extension SearchCoordinator: SearchCoordinating {
public func toRequests(from parent: UIViewController) {
let screen = requestsFactory()
replacePresenter.present(screen, from: parent)
}
public func toContacts(from parent: UIViewController) {
let screen = contactsFactory()
replacePresenter.present(screen, from: parent)
}
public func toContact(_ contact: Contact, from parent: UIViewController) {
let screen = contactFactory(contact)
pushPresenter.present(screen, from: parent)
......
import Shared
import Combine
import XXModels
import Foundation
import Permissions
import Integration
import DependencyInjection
enum SearchQRStatus: Equatable {
enum ScanningStatus: Equatable {
case reading
case processing
case success
case failed(SearchQRError)
case failed(ScanningError)
}
enum SearchQRError: Equatable {
enum ScanningError: Equatable {
case requestOpened
case unknown(String)
case cameraPermission
......@@ -16,97 +21,91 @@ enum SearchQRError: Equatable {
}
final class SearchRightViewModel {
@Dependency private var permissions: PermissionHandling
}
@Dependency var session: SessionType
@Dependency var permissions: PermissionHandling
var foundPublisher: AnyPublisher<Contact, Never> {
foundSubject.eraseToAnyPublisher()
}
var cameraSemaphorePublisher: AnyPublisher<Bool, Never> {
cameraSemaphoreSubject.eraseToAnyPublisher()
}
var statusPublisher: AnyPublisher<ScanningStatus, Never> {
statusSubject.eraseToAnyPublisher()
}
private let foundSubject = PassthroughSubject<Contact, Never>()
private let cameraSemaphoreSubject = PassthroughSubject<Bool, Never>()
private(set) var statusSubject = CurrentValueSubject<ScanningStatus, Never>(.reading)
func viewDidAppear() {
permissions.requestCamera { [weak self] granted in
guard let self = self else { return }
if granted {
self.statusSubject.value = .reading
self.cameraSemaphoreSubject.send(true)
} else {
self.statusSubject.send(.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()
// }
//}
func viewWillDisappear() {
cameraSemaphoreSubject.send(false)
}
func didScan(data: Data) {
/// We need to be accepting new readings in order
/// to process what just got scanned.
///
guard statusSubject.value == .reading else { return }
statusSubject.send(.processing)
/// Whatever got scanned, needs to have id and username
/// otherwise is just noise or an unknown qr code
///
guard let userId = session.getId(from: data),
let username = try? session.extract(fact: .username, from: data) else {
let errorTitle = Localized.Scan.Error.invalid
statusSubject.send(.failed(.unknown(errorTitle)))
return
}
/// Make sure we are not processing a contact
/// that we already have
///
if let alreadyContact = try? session.dbManager.fetchContacts(.init(id: [userId])).first {
/// Show error accordingly to the auth status
///
if alreadyContact.authStatus == .friend {
statusSubject.send(.failed(.alreadyFriends(username)))
} else if [.requested, .verified].contains(alreadyContact.authStatus) {
statusSubject.send(.failed(.requestOpened))
} else {
let generalErrorTitle = Localized.Scan.Error.general
statusSubject.send(.failed(.unknown(generalErrorTitle)))
}
return
}
statusSubject.send(.success)
cameraSemaphoreSubject.send(false)
foundSubject.send(.init(
id: userId,
marshaled: data,
username: username,
email: try? session.extract(fact: .email, from: data),
phone: try? session.extract(fact: .phone, from: data),
nickname: nil,
photo: nil,
authStatus: .stranger,
isRecent: false,
createdAt: Date()
))
}
}
......@@ -11,7 +11,6 @@ final class SearchRightView: UIView {
init() {
super.init(frame: .zero)
imageView.contentMode = .center
actionButton.setStyle(.brandColored)
......@@ -39,6 +38,67 @@ final class SearchRightView: UIView {
required init?(coder: NSCoder) { nil }
func update(status: ScanningStatus) {
var text: String
switch status {
case .reading, .processing:
imageView.isHidden = true
actionButton.isHidden = true
text = Localized.Scan.Status.reading
overlayView.updateCornerColor(Asset.brandPrimary.color)
case .success:
animationView.isHidden = true
actionButton.isHidden = true
imageView.isHidden = false
imageView.image = Asset.sharedSuccess.image
text = Localized.Scan.Status.success
overlayView.updateCornerColor(Asset.accentSuccess.color)
case .failed(let error):
animationView.isHidden = true
imageView.image = Asset.scanError.image
imageView.isHidden = false
overlayView.updateCornerColor(Asset.accentDanger.color)
switch error {
case .requestOpened:
text = Localized.Scan.Error.requested
actionButton.setTitle(Localized.Scan.requests, for: .normal)
actionButton.isHidden = false
case .alreadyFriends(let name):
text = Localized.Scan.Error.friends(name)
actionButton.setTitle(Localized.Scan.contact, for: .normal)
actionButton.isHidden = false
case .cameraPermission:
text = Localized.Scan.Error.denied
actionButton.setTitle(Localized.Scan.settings, for: .normal)
actionButton.isHidden = false
case .unknown(let content):
text = content
}
}
let attString = NSMutableAttributedString(string: text)
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
paragraph.lineHeightMultiple = 1.35
attString.addAttribute(.paragraphStyle, value: paragraph)
attString.addAttribute(.foregroundColor, value: Asset.neutralWhite.color)
attString.addAttribute(.font, value: Fonts.Mulish.regular.font(size: 14.0) as Any)
if text.contains("#") {
attString.addAttribute(name: .foregroundColor, value: Asset.brandPrimary.color, betweenCharacters: "#")
}
statusLabel.attributedText = attString
}
private func setupConstraints() {
overlayView.snp.makeConstraints {
$0.top.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