Implement ScannerViewController and new Contact creation.

This commit is contained in:
CypherPoet 2020-01-14 17:41:55 -06:00
parent 2546be8bcb
commit 43baab40d4
12 changed files with 486 additions and 18 deletions

View File

@ -40,6 +40,11 @@
F3203BEF23CC985100265268 /* UserQRCodeFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BEE23CC985100265268 /* UserQRCodeFormView.swift */; };
F3203BF123CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BF023CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift */; };
F3203BF423CD140100265268 /* ContactQRCodeRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BF323CD140100265268 /* ContactQRCodeRepresentable.swift */; };
F3203BFB23CD430500265268 /* QRCodeScannerView+Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BF923CD430500265268 /* QRCodeScannerView+Coordinator.swift */; };
F3203BFC23CD430500265268 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BFA23CD430500265268 /* QRCodeScannerView.swift */; };
F3203BFF23CDF6D100265268 /* ScannerViewControlller.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BFE23CDF6D100265268 /* ScannerViewControlller.swift */; };
F326140823CE3928009EC215 /* ContactsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326140723CE3928009EC215 /* ContactsState.swift */; };
F326140A23CE689A009EC215 /* Contact+InitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F326140923CE689A009EC215 /* Contact+InitHelpers.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -75,6 +80,11 @@
F3203BEE23CC985100265268 /* UserQRCodeFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserQRCodeFormView.swift; sourceTree = "<group>"; };
F3203BF023CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserQRCodeFormView+ViewModel.swift"; sourceTree = "<group>"; };
F3203BF323CD140100265268 /* ContactQRCodeRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactQRCodeRepresentable.swift; sourceTree = "<group>"; };
F3203BF923CD430500265268 /* QRCodeScannerView+Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QRCodeScannerView+Coordinator.swift"; sourceTree = "<group>"; };
F3203BFA23CD430500265268 /* QRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScannerView.swift; sourceTree = "<group>"; };
F3203BFE23CDF6D100265268 /* ScannerViewControlller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerViewControlller.swift; sourceTree = "<group>"; };
F326140723CE3928009EC215 /* ContactsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsState.swift; sourceTree = "<group>"; };
F326140923CE689A009EC215 /* Contact+InitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contact+InitHelpers.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -134,10 +144,10 @@
F3203B9823C760BF00265268 /* App */ = {
isa = PBXGroup;
children = (
F3203B9D23C7610400265268 /* LaunchScreen.storyboard */,
F3203B8323C7609500265268 /* SceneDelegate.swift */,
F3203B8123C7609500265268 /* AppDelegate.swift */,
F3203BA923C82CB100265268 /* CurrentApplication.swift */,
F3203B8323C7609500265268 /* SceneDelegate.swift */,
F3203B9D23C7610400265268 /* LaunchScreen.storyboard */,
);
path = App;
sourceTree = "<group>";
@ -145,9 +155,9 @@
F3203B9923C760C300265268 /* Scenes */ = {
isa = PBXGroup;
children = (
F3203BB023C8345400265268 /* RootView.swift */,
F3203BAD23C8339400265268 /* Collected Contacts */,
F3203BAC23C8337F00265268 /* User QR Code */,
F3203BB023C8345400265268 /* RootView.swift */,
);
path = Scenes;
sourceTree = "<group>";
@ -155,10 +165,10 @@
F3203B9A23C760C700265268 /* Data */ = {
isa = PBXGroup;
children = (
F3203BAB23C831E300265268 /* Models */,
F3203BA823C82C8F00265268 /* State */,
F3203B8523C7609500265268 /* QRConnections.xcdatamodeld */,
F3203BA623C7636600265268 /* CoreDataManager+Utils.swift */,
F3203BAB23C831E300265268 /* Models */,
F3203B8523C7609500265268 /* QRConnections.xcdatamodeld */,
F3203BA823C82C8F00265268 /* State */,
);
path = Data;
sourceTree = "<group>";
@ -166,8 +176,9 @@
F3203B9B23C760C900265268 /* Reusables */ = {
isa = PBXGroup;
children = (
F3203BF223CD13F300265268 /* Protocols */,
F3203BF823CD42F400265268 /* UIKit Wrappers */,
F3203BD023C9DFDF00265268 /* ImageFilterService.swift */,
F3203BF223CD13F300265268 /* Protocols */,
);
path = Reusables;
sourceTree = "<group>";
@ -185,6 +196,7 @@
children = (
F3203BD223C9F20C00265268 /* AppState.swift */,
F3203BD423C9F3E900265268 /* UserProfileState.swift */,
F326140723CE3928009EC215 /* ContactsState.swift */,
);
path = State;
sourceTree = "<group>";
@ -200,11 +212,11 @@
F3203BAC23C8337F00265268 /* User QR Code */ = {
isa = PBXGroup;
children = (
F3203BCE23C9DAE000265268 /* UserQRCodeView.swift */,
F3203BEA23CC977000265268 /* UserQRCodeContainerView.swift */,
F3203BEE23CC985100265268 /* UserQRCodeFormView.swift */,
F3203BEC23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift */,
F3203BEE23CC985100265268 /* UserQRCodeFormView.swift */,
F3203BF023CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift */,
F3203BCE23C9DAE000265268 /* UserQRCodeView.swift */,
);
path = "User QR Code";
sourceTree = "<group>";
@ -213,8 +225,8 @@
isa = PBXGroup;
children = (
F3203BB223C8368800265268 /* ContactsContainerView.swift */,
F3203BB623C844F100265268 /* ContactsListView.swift */,
F3203BB423C841A700265268 /* ContactsContainerView+ViewModel.swift */,
F3203BB623C844F100265268 /* ContactsListView.swift */,
);
path = "Collected Contacts";
sourceTree = "<group>";
@ -225,6 +237,7 @@
F3203BB823C8467000265268 /* Contact+FilterState.swift */,
F3203BBB23C8579400265268 /* Contact+CoreDataClass.swift */,
F3203BBC23C8579400265268 /* Contact+CoreDataProperties.swift */,
F326140923CE689A009EC215 /* Contact+InitHelpers.swift */,
F3203BC123C8588E00265268 /* Contact+FetchHelpers.swift */,
F3203BBF23C8584500265268 /* Contact+Status.swift */,
);
@ -249,6 +262,24 @@
path = Protocols;
sourceTree = "<group>";
};
F3203BF823CD42F400265268 /* UIKit Wrappers */ = {
isa = PBXGroup;
children = (
F3203BFD23CDF68D00265268 /* Scanner View */,
);
path = "UIKit Wrappers";
sourceTree = "<group>";
};
F3203BFD23CDF68D00265268 /* Scanner View */ = {
isa = PBXGroup;
children = (
F3203BF923CD430500265268 /* QRCodeScannerView+Coordinator.swift */,
F3203BFA23CD430500265268 /* QRCodeScannerView.swift */,
F3203BFE23CDF6D100265268 /* ScannerViewControlller.swift */,
);
path = "Scanner View";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -334,11 +365,13 @@
F3203BB323C8368800265268 /* ContactsContainerView.swift in Sources */,
F3203BC923C871D300265268 /* PreviewDevice+Utils.swift in Sources */,
F3203BB923C8467000265268 /* Contact+FilterState.swift in Sources */,
F3203BFB23CD430500265268 /* QRCodeScannerView+Coordinator.swift in Sources */,
F3203BC223C8588F00265268 /* Contact+FetchHelpers.swift in Sources */,
F3203BC023C8584500265268 /* Contact+Status.swift in Sources */,
F3203BA723C7636600265268 /* CoreDataManager+Utils.swift in Sources */,
F3203BCF23C9DAE100265268 /* UserQRCodeView.swift in Sources */,
F3203B8223C7609500265268 /* AppDelegate.swift in Sources */,
F326140823CE3928009EC215 /* ContactsState.swift in Sources */,
F3203BB123C8345400265268 /* RootView.swift in Sources */,
F3203BC723C8717F00265268 /* SampleData+Contacts.swift in Sources */,
F3203BD323C9F20C00265268 /* AppState.swift in Sources */,
@ -346,9 +379,11 @@
F3203BB723C844F100265268 /* ContactsListView.swift in Sources */,
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */,
F3203BB523C841A700265268 /* ContactsContainerView+ViewModel.swift in Sources */,
F326140A23CE689A009EC215 /* Contact+InitHelpers.swift in Sources */,
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */,
F3203BF123CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift in Sources */,
F3203BEF23CC985100265268 /* UserQRCodeFormView.swift in Sources */,
F3203BFF23CDF6D100265268 /* ScannerViewControlller.swift in Sources */,
F3203BE223CA31EB00265268 /* SampleData+AppStore.swift in Sources */,
F3203BD123C9DFDF00265268 /* ImageFilterService.swift in Sources */,
F3203B8723C7609500265268 /* QRConnections.xcdatamodeld in Sources */,
@ -357,6 +392,7 @@
F3203BC523C8714700265268 /* SampleData.swift in Sources */,
F3203BD523C9F3E900265268 /* UserProfileState.swift in Sources */,
F3203B8423C7609500265268 /* SceneDelegate.swift in Sources */,
F3203BFC23CD430500265268 /* QRCodeScannerView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git",
"state": {
"branch": null,
"revision": "5400fc3983f4174cd36cc79b24b7ef54affecf6b",
"version": "0.0.5"
"revision": "e14a3abf3e679d41c9e0fcc55d3e4d7ccd8ce1b6",
"version": "0.0.6"
}
},
{

View File

@ -9,6 +9,7 @@
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

View File

@ -0,0 +1,28 @@
//
// Contact+InitHelpers.swift
// QRConnections
//
// Created by CypherPoet on 1/14/20.
//
//
import Foundation
import CoreData
extension Contact {
static func make(
fromName name: String,
andStatus status: Status = .uncontacted,
using context: NSManagedObjectContext
) -> Contact {
let contact = Contact(context: context)
contact.name = name
contact.uuid = UUID()
contact.status = status
return contact
}
}

View File

@ -12,6 +12,7 @@ import CypherPoetSwiftUIKit_DataFlowUtils
struct AppState {
var contactsState = ContactsState()
var userProfileState = UserProfileState()
}
@ -22,6 +23,7 @@ struct AppState {
enum AppAction {
case contacts(_ action: ContactsAction)
case userProfile(_ action: UserProfileAction)
}
@ -31,6 +33,8 @@ let appReducer = Reducer<AppState, AppAction> { appState, action in
switch action {
case .userProfile(let action):
userProfileReducer.reduce(&appState.userProfileState, action)
case .contacts(let action):
contactsReducer.reduce(&appState.contactsState, action)
}
}

View File

@ -0,0 +1,63 @@
//
// ContactsState.swift
// QRConnections
//
// Created by CypherPoet on 1/14/20.
//
//
import Foundation
import Combine
import CypherPoetSwiftUIKit_DataFlowUtils
struct ContactsState {
var saveErrorMessage: String?
}
enum ContactsSideEffect: SideEffect {
case save(_ contact: Contact)
func mapToAction() -> AnyPublisher<AppAction, Never> {
switch self {
case .save(let contact):
guard let context = contact.managedObjectContext else {
return Just(AppAction.contacts(.saveErrorMessageSet("No managed object context found")))
.eraseToAnyPublisher()
}
return Just(context)
.flatMap { context in
CurrentApp.coreDataManager.save(context)
.map { AppAction.contacts(.saveErrorMessageSet(nil)) }
.catch { error in
Just(AppAction.contacts(.saveErrorMessageSet(error.localizedDescription)))
}
}
.eraseToAnyPublisher()
}
}
}
enum ContactsAction {
case saveErrorMessageSet(String?)
}
// MARK: - Reducer
let contactsReducer: Reducer<ContactsState, ContactsAction> = Reducer(
reduce: { state, action in
switch action {
case .saveErrorMessageSet(let message):
state.saveErrorMessage = message
}
}
)

View File

@ -49,6 +49,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app would like to scan QR codes in order to add to your collection of contacts.</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@ -0,0 +1,60 @@
//
// QRCodeScannerView+Coordinator.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import Foundation
import UIKit
import AVFoundation
// MARK: - Coordinator
extension QRCodeScannerView {
class Coordinator: NSObject {
let onScanCompleted: QRCodeScannerView.CompletionHandler
let simulatedData: String
init(
simulatedData: String,
onScanCompleted: @escaping QRCodeScannerView.CompletionHandler
) {
self.simulatedData = simulatedData
self.onScanCompleted = onScanCompleted
}
}
}
// MARK: - ScannerViewControllerCaptureSessionDelegate
extension QRCodeScannerView.Coordinator: ScannerViewControllerCaptureSessionDelegate {
func captureSession(didScan metadataObject: ScannerViewController.CapturedOutput) {
#if targetEnvironment(simulator)
onScanCompleted(.success(simulatedData))
#else
guard
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else
{ return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
onScanCompleted(.success(stringValue))
#endif
}
func captureSessionWasUnableToAddInput() {
onScanCompleted(.failure(.badInput))
}
func captureSessionWasUnableToAddOutput() {
onScanCompleted(.failure(.badOutput))
}
}

View File

@ -0,0 +1,70 @@
//
// QRCodeScannerView.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import SwiftUI
import AVFoundation
struct QRCodeScannerView {
typealias UIViewControllerType = ScannerViewController
typealias Completion = Result<String, ScanError>
typealias CompletionHandler = ((Completion) -> Void)
var codeTypes: [AVMetadataObject.ObjectType] = [.qr]
var simulatedData = ""
let onScanCompleted: CompletionHandler
}
extension QRCodeScannerView {
enum ScanError: Error {
case badInput
case badOutput
}
}
// MARK: - UIViewControllerRepresentable
extension QRCodeScannerView: UIViewControllerRepresentable {
func makeCoordinator() -> Self.Coordinator {
Self.Coordinator(
simulatedData: simulatedData,
onScanCompleted: onScanCompleted
)
}
func makeUIViewController(
context: UIViewControllerRepresentableContext<QRCodeScannerView>
) -> UIViewControllerType {
let viewController = ScannerViewController()
viewController.codeTypes = codeTypes
viewController.captureSessionDelegate = context.coordinator
return viewController
}
func updateUIViewController(
_ uiViewController: UIViewControllerType,
context: UIViewControllerRepresentableContext<QRCodeScannerView>
) {
}
}
// MARK: - Preview
struct QRCodeScannerView_Previews: PreviewProvider {
static var previews: some View {
QRCodeScannerView(onScanCompleted: { _ in })
}
}

View File

@ -0,0 +1,171 @@
//
// ScannerViewControlller.swift
// QRConnections
//
// Created by CypherPoet on 1/14/20.
//
//
import AVFoundation
import UIKit
protocol ScannerViewControllerCaptureSessionDelegate: class {
func captureSession(didScan metadataObject: ScannerViewController.CapturedOutput)
func captureSessionWasUnableToAddInput()
func captureSessionWasUnableToAddOutput()
}
public final class ScannerViewController: UIViewController {
private var captureSession: AVCaptureSession!
private var previewLayer: AVCaptureVideoPreviewLayer!
var codeTypes: [AVMetadataObject.ObjectType] = [.qr]
weak var captureSessionDelegate: ScannerViewControllerCaptureSessionDelegate?
}
extension ScannerViewController {
#if targetEnvironment(simulator)
typealias CapturedOutput = String
#else
typealias CapturedOutput = AVMetadataObject
#endif
}
// MARK: - Computeds
extension ScannerViewController {
override public var prefersStatusBarHidden: Bool {
return true
}
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
}
// MARK: - View Controller Lifecycle
#if targetEnvironment(simulator)
extension ScannerViewController {
override public func loadView() {
view = UIView()
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = "You're running in the simulator, which means the camera isn't available. Tap anywhere to send back some simulated data."
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
label.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
label.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
])
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
captureSessionDelegate?.captureSession(didScan: "")
dismiss(animated: true)
}
}
#else
extension ScannerViewController {
override public func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
guard let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else { return }
if (captureSession.canAddInput(videoInput)) {
captureSession.addInput(videoInput)
} else {
captureSessionDelegate?.captureSessionWasUnableToAddInput()
return
}
let metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = codeTypes
} else {
captureSessionDelegate?.captureSessionWasUnableToAddOutput()
return
}
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
captureSession.startRunning()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if (captureSession?.isRunning == false) {
captureSession.startRunning()
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if (captureSession?.isRunning == true) {
captureSession.stopRunning()
}
}
}
#endif
#if targetEnvironment(simulator)
#else
extension ScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
public func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
captureSession.stopRunning()
if let metadataObject = metadataObjects.first {
captureSessionDelegate?.captureSession(didScan: metadataObject)
}
dismiss(animated: true)
}
}
#endif

View File

@ -9,13 +9,17 @@
import SwiftUI
struct ContactsContainerView: View {
struct ContactsContainerView {
@EnvironmentObject private var store: AppStore
@ObservedObject var viewModel = ViewModel()
@State private var isShowingScannerView = false
}
// MARK: - Body
extension ContactsContainerView {
// MARK: - View
extension ContactsContainerView: View {
var body: some View {
NavigationView {
@ -27,6 +31,12 @@ extension ContactsContainerView {
}
.navigationBarTitle("Collected Contacts")
.navigationBarItems(trailing: addContactButton)
.sheet(isPresented: $isShowingScannerView) {
QRCodeScannerView(
simulatedData: "🚀 Rocket Man",
onScanCompleted: self.codeScanCompleted(_:)
)
}
}
}
}
@ -56,7 +66,7 @@ extension ContactsContainerView {
private var addContactButton: some View {
Button(action: {
self.isShowingScannerView = true
}) {
Image(systemName: "qrcode.viewfinder")
.imageScale(.large)
@ -66,6 +76,30 @@ extension ContactsContainerView {
}
// MARK: - Private Helpers
private extension ContactsContainerView {
func codeScanCompleted(_ result: QRCodeScannerView.Completion) {
switch result {
case .success(let qrCodeString):
createNewContact(from: qrCodeString)
// self.store.send(ContactsSideEffect.createContact(qrCodeString))
case .failure(let error):
print("Scanning failed. Error \(error.localizedDescription)")
}
}
func createNewContact(from qrCodeString: String) {
let context = CurrentApp.coreDataManager.backgroundContext
let contact = Contact.make(fromName: qrCodeString, using: context)
self.store.send(ContactsSideEffect.save(contact))
}
}
// MARK: - Preview
struct ContactsContainerView_Previews: PreviewProvider {
@ -74,5 +108,6 @@ struct ContactsContainerView_Previews: PreviewProvider {
viewModel: .init()
)
.environment(\.managedObjectContext, CurrentApp.coreDataManager.mainContext)
.environmentObject(SampleData.appStore)
}
}

View File

@ -25,7 +25,6 @@ struct RootView: View {
extension RootView {
var body: some View {
TabView {
ContactsContainerView(viewModel: .init())
.tabItem {
@ -42,7 +41,6 @@ extension RootView {
}
.tag(Tab.userQRCode)
}
// .environmentObject(store)
.edgesIgnoringSafeArea(.top)
}
}