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

Continued working on search ui

parent 29a95fac
No related branches found
No related tags found
2 merge requests!54Releasing 1.1.4,!50Search UI 2.0
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 SearchQRController: UIViewController {
@Dependency private var permissions: PermissionHandling
lazy private var screenView = SearchQRView()
private let camera = Camera()
private var status: SearchQRStatus?
override func loadView() {
view = screenView
}
override func viewDidLoad() {
super.viewDidLoad()
///screenView.layer.insertSublayer(camera.previewLayer, at: 0)
///setupBindings()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
///camera.previewLayer.frame = screenView.bounds
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
///viewModel.resetScanner()
///startCamera()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// backgroundScheduler.schedule { [weak self] in
// guard let self = self else { return }
// self.camera.stop()
// }
}
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()
}
}
import UIKit
import Shared
final class OverlayView: UIView {
private let cropView = UIView()
private let scanViewLength = 266.0
private let maskLayer = CAShapeLayer()
private let topLeftLayer = CAShapeLayer()
private let topRightLayer = CAShapeLayer()
private let bottomLeftLayer = CAShapeLayer()
private let bottomRightLayer = CAShapeLayer()
init() {
super.init(frame: .zero)
backgroundColor = Asset.neutralDark.color.withAlphaComponent(0.5)
addSubview(cropView)
cropView.snp.makeConstraints {
$0.width.equalTo(scanViewLength)
$0.centerY.equalToSuperview().offset(-50)
$0.centerX.equalToSuperview()
$0.height.equalTo(scanViewLength)
}
maskLayer.fillRule = .evenOdd
layer.mask = maskLayer
layer.masksToBounds = true
[topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach {
$0.strokeColor = Asset.brandPrimary.color.cgColor
$0.fillColor = UIColor.clear.cgColor
$0.lineWidth = 3.0
$0.lineCap = .round
layer.addSublayer($0)
}
}
required init?(coder: NSCoder) { nil }
override func layoutSubviews() {
super.layoutSubviews()
maskLayer.frame = bounds
let path = UIBezierPath(rect: bounds)
path.append(UIBezierPath(roundedRect: cropView.frame, cornerRadius: 30.0))
maskLayer.path = path.cgPath
topLeftLayer.frame = bounds
topRightLayer.frame = bounds
bottomRightLayer.frame = bounds
bottomLeftLayer.frame = bounds
topLeftLayer.path = topLeftPath()
topRightLayer.path = topRightPath()
bottomRightLayer.path = bottomRightPath()
bottomLeftLayer.path = bottomLeftPath()
}
func updateCornerColor(_ color: UIColor) {
[topLeftLayer, topRightLayer, bottomLeftLayer, bottomRightLayer].forEach {
$0.strokeColor = color.cgColor
}
}
func topLeftPath() -> CGPath {
let path = UIBezierPath()
let vert0X = cropView.frame.minX - 15
let vert0Y = cropView.frame.minY + 45
let vert0 = CGPoint(x: vert0X, y: vert0Y)
path.move(to: vert0)
let vertNX = cropView.frame.minX - 15
let vertNY = cropView.frame.minY + 15
let vertN = CGPoint(x: vertNX, y: vertNY)
path.addLine(to: vertN)
let arcCenterX = cropView.frame.minX + 15
let arcCenterY = cropView.frame.minY + 15
let arcCenter = CGPoint(x: arcCenterX , y: arcCenterY)
path.addArc(center: arcCenter, startAngle: .pi)
let horizX = cropView.frame.minX + 45
let horizY = cropView.frame.minY - 15
let horiz = CGPoint(x: horizX, y: horizY)
path.addLine(to: horiz)
return path.cgPath
}
func topRightPath() -> CGPath {
let path = UIBezierPath()
let horiz0X = cropView.frame.maxX - 45
let horiz0Y = cropView.frame.minY - 15
let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
path.move(to: horiz0)
let horizNX = cropView.frame.maxX - 15
let horizNY = cropView.frame.minY - 15
let horizN = CGPoint(x: horizNX, y: horizNY)
path.addLine(to: horizN)
let arcCenterX = cropView.frame.maxX - 15
let arcCenterY = cropView.frame.minY + 15
let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
path.addArc(center: arcCenter, startAngle: 3 * .pi/2)
let vertX = cropView.frame.maxX + 15
let vertY = cropView.frame.minY + 45
let vert = CGPoint(x: vertX, y: vertY)
path.addLine(to: vert)
return path.cgPath
}
func bottomRightPath() -> CGPath {
let path = UIBezierPath()
let vert0X = cropView.frame.maxX + 15
let vert0Y = cropView.frame.maxY - 45
let vert0 = CGPoint(x: vert0X, y: vert0Y)
path.move(to: vert0)
let vertNX = cropView.frame.maxX + 15
let vertNY = cropView.frame.maxY - 15
let vertN = CGPoint(x: vertNX, y: vertNY)
path.addLine(to: vertN)
let arcCenterX = cropView.frame.maxX - 15
let arcCenterY = cropView.frame.maxY - 15
let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
path.addArc(center: arcCenter, startAngle: 0)
let horizX = cropView.frame.maxX - 45
let horizY = cropView.frame.maxY + 15
let horiz = CGPoint(x: horizX, y: horizY)
path.addLine(to: horiz)
return path.cgPath
}
func bottomLeftPath() -> CGPath {
let path = UIBezierPath()
let horiz0X = cropView.frame.minX + 45
let horiz0Y = cropView.frame.maxY + 15
let horiz0 = CGPoint(x: horiz0X, y: horiz0Y)
path.move(to: horiz0)
let horizNX = cropView.frame.minX + 15
let horizNY = cropView.frame.maxY + 15
let horizN = CGPoint(x: horizNX, y: horizNY)
path.addLine(to: horizN)
let arcCenterX = cropView.frame.minX + 15
let arcCenterY = cropView.frame.maxY - 15
let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
path.addArc(center: arcCenter, startAngle: .pi/2)
let vertX = cropView.frame.minX - 15
let vertY = cropView.frame.maxY - 45
let vert = CGPoint(x: vertX, y: vertY)
path.addLine(to: vert)
return path.cgPath
}
}
private extension UIBezierPath {
func addArc(center: CGPoint, startAngle: CGFloat) {
addArc(
withCenter: center,
radius: 30,
startAngle: startAngle,
endAngle: startAngle + .pi/2,
clockwise: true
)
}
}
import UIKit
import Shared
import InputField
final class SearchEmailView: UIView {
let inputField = InputField()
let inputField = SearchComponent()
init() {
super.init(frame: .zero)
inputField.setup(
style: .regular,
title: "Email",
placeholder: "Email"
inputField.set(
placeholder: Localized.Ud.Search.Email.input,
imageAtRight: nil
)
addSubview(inputField)
......
import UIKit
import Shared
import InputField
final class SearchPhoneView: UIView {
let inputField = InputField()
let inputField = SearchComponent()
init() {
super.init(frame: .zero)
inputField.setup(
style: .regular,
title: "Phone",
placeholder: "Phone"
inputField.set(
placeholder: Localized.Ud.Search.Phone.input,
imageAtRight: nil
)
addSubview(inputField)
......
import UIKit
import Shared
import InputField
final class SearchQRView: UIView {
let inputField = InputField()
let statusLabel = UILabel()
let imageView = UIImageView()
let stackView = UIStackView()
let animationView = DotAnimation()
let overlayView = OverlayView()
let actionButton = CapsuleButton()
init() {
super.init(frame: .zero)
inputField.setup(
style: .regular,
title: "QR",
placeholder: "QR"
)
imageView.contentMode = .center
actionButton.setStyle(.brandColored)
addSubview(inputField)
statusLabel.numberOfLines = 0
statusLabel.textAlignment = .center
statusLabel.textColor = Asset.neutralWhite.color
statusLabel.font = Fonts.Mulish.regular.font(size: 14.0)
inputField.snp.makeConstraints {
$0.top.equalToSuperview().offset(15)
$0.left.equalToSuperview().offset(15)
$0.right.equalToSuperview().offset(-15)
$0.bottom.lessThanOrEqualToSuperview()
}
stackView.spacing = 15
stackView.axis = .vertical
stackView.addArrangedSubview(animationView)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(statusLabel)
stackView.addArrangedSubview(actionButton)
imageView.isHidden = true
actionButton.isHidden = true
animationView.isHidden = false
addSubview(overlayView)
addSubview(stackView)
setupConstraints()
}
required init?(coder: NSCoder) { nil }
private func setupConstraints() {
overlayView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.left.equalToSuperview()
$0.right.equalToSuperview()
$0.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
$0.left.equalToSuperview().offset(57)
$0.right.equalToSuperview().offset(-57)
$0.bottom.equalTo(safeAreaLayoutGuide).offset(-100)
}
}
}
......@@ -16,7 +16,7 @@ final class SearchUsernamePlaceholderView: UIView {
super.init(frame: .zero)
let attrString = NSMutableAttributedString(
string: Localized.Ud.Username.Search.Placeholder.title,
string: Localized.Ud.Search.Username.Placeholder.title,
attributes: [
.foregroundColor: Asset.neutralDark.color,
.font: Fonts.Mulish.bold.font(size: 32.0)
......@@ -36,7 +36,7 @@ final class SearchUsernamePlaceholderView: UIView {
paragraph.lineHeightMultiple = 1.3
subtitleWithInfo.setup(
text: Localized.Ud.Username.Search.Placeholder.subtitle,
text: Localized.Ud.Search.Username.Placeholder.subtitle,
attributes: [
.paragraphStyle: paragraph,
.foregroundColor: Asset.neutralBody.color,
......
import UIKit
import Shared
import InputField
final class SearchUsernameView: UIView {
let inputField = SearchComponent()
......@@ -10,7 +9,7 @@ final class SearchUsernameView: UIView {
super.init(frame: .zero)
inputField.set(
placeholder: Localized.Ud.Username.Search.inputPlaceholder,
placeholder: Localized.Ud.Search.Username.input,
imageAtRight: nil
)
......
......@@ -1205,8 +1205,6 @@ public enum Localized {
public static func noneFound(_ p1: Any) -> String {
return Localized.tr("Localizable", "ud.noneFound", String(describing: p1))
}
/// User
public static let sectionTitle = Localized.tr("Localizable", "ud.sectionTitle")
/// Search
public static let title = Localized.tr("Localizable", "ud.title")
public enum NicknameDrawer {
......@@ -1218,8 +1216,6 @@ public enum Localized {
public static let title = Localized.tr("Localizable", "ud.nicknameDrawer.title")
}
public enum Placeholder {
/// Searching is private by nature. The network cannot identify who a search request came from.
public static let title = Localized.tr("Localizable", "ud.placeholder.title")
public enum Drawer {
/// Got it
public static let action = Localized.tr("Localizable", "ud.placeholder.drawer.action")
......@@ -1241,6 +1237,26 @@ public enum Localized {
/// Request Contact
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")
}
public enum Phone {
/// Search by phone number
public static let input = Localized.tr("Localizable", "ud.search.phone.input")
}
public enum Username {
/// Search by username
public static let input = Localized.tr("Localizable", "ud.search.username.input")
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 Tab {
/// Email
public static let email = Localized.tr("Localizable", "ud.tab.email")
......@@ -1251,18 +1267,6 @@ public enum Localized {
/// Username
public static let username = Localized.tr("Localizable", "ud.tab.username")
}
public enum Username {
public enum Search {
/// Search by username
public static let inputPlaceholder = Localized.tr("Localizable", "ud.username.search.inputPlaceholder")
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.username.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.username.search.placeholder.title")
}
}
}
}
public enum Validator {
......
......@@ -950,24 +950,20 @@
"ud.title"
= "Search";
"ud.placeholder.title"
= "Searching is private by nature. The network cannot identify who a search request came from.";
"ud.tab.username"
= "Username";
"ud.tab.email"
= "Email";
"ud.tab.phone"
= "Phone";
"ud.tab.qr"
= "QR Code";
"ud.placeholder.drawer.title"
= "Search";
"ud.placeholder.drawer.subtitle"
= "You can search for users by their username, email, or phone number using the xx network’s #Anonymous Data Retrieval protocol# which keeps a user’s identity anonymous while requesting data. All sent requests contain salted hashes of what you are searching for. Raw data on emails, usernames, and phone numbers do not leave your phone.";
"ud.placeholder.drawer.action"
= "Got it";
"ud.tab.phone"
= "Phone";
"ud.tab.email"
= "Email";
"ud.tab.username"
= "Username";
"ud.tab.qr"
= "QR Code";
"ud.sectionTitle"
= "User";
"ud.noneFound"
= "There are no users with that %@.";
"ud.requestDrawer.title"
......@@ -986,12 +982,16 @@
= "Edit your new contact’s nickname so you know who they are.";
"ud.nicknameDrawer.save"
= "Save";
"ud.username.search.inputPlaceholder"
"ud.search.username.input"
= "Search by username";
"ud.username.search.placeholder.title"
"ud.search.email.input"
= "Search by email";
"ud.search.phone.input"
= "Search by phone number";
"ud.search.username.placeholder.title"
= "Search for #friends# anonymously, add them to your #connections# to start a completely private messaging channel.";
"ud.username.search.placeholder.subtitle"
"ud.search.username.placeholder.subtitle"
= "Your searches are anonymous. Search information is never linked to your account or personally identifiable.";
// LaunchFeature
......
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