Build out views and data flow for creating the users' QR code image.

This commit is contained in:
CypherPoet 2020-01-13 15:11:50 -06:00
parent de8ee275cb
commit 5d0c36cb09
20 changed files with 734 additions and 16 deletions

View File

@ -29,6 +29,17 @@
F3203BC523C8714700265268 /* SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BC423C8714700265268 /* SampleData.swift */; };
F3203BC723C8717F00265268 /* SampleData+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BC623C8717F00265268 /* SampleData+Contacts.swift */; };
F3203BC923C871D300265268 /* PreviewDevice+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BC823C871D300265268 /* PreviewDevice+Utils.swift */; };
F3203BCF23C9DAE100265268 /* UserQRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BCE23C9DAE000265268 /* UserQRCodeView.swift */; };
F3203BD123C9DFDF00265268 /* ImageFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BD023C9DFDF00265268 /* ImageFilterService.swift */; };
F3203BD323C9F20C00265268 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BD223C9F20C00265268 /* AppState.swift */; };
F3203BD523C9F3E900265268 /* UserProfileState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BD423C9F3E900265268 /* UserProfileState.swift */; };
F3203BE223CA31EB00265268 /* SampleData+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BE123CA31EB00265268 /* SampleData+AppStore.swift */; };
F3203BE523CA412600265268 /* Burritos in Frameworks */ = {isa = PBXBuildFile; productRef = F3203BE423CA412600265268 /* Burritos */; };
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BEA23CC977000265268 /* UserQRCodeContainerView.swift */; };
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3203BEC23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift */; };
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 */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -54,6 +65,16 @@
F3203BC423C8714700265268 /* SampleData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleData.swift; sourceTree = "<group>"; };
F3203BC623C8717F00265268 /* SampleData+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SampleData+Contacts.swift"; sourceTree = "<group>"; };
F3203BC823C871D300265268 /* PreviewDevice+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreviewDevice+Utils.swift"; sourceTree = "<group>"; };
F3203BCE23C9DAE000265268 /* UserQRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserQRCodeView.swift; sourceTree = "<group>"; };
F3203BD023C9DFDF00265268 /* ImageFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterService.swift; sourceTree = "<group>"; };
F3203BD223C9F20C00265268 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
F3203BD423C9F3E900265268 /* UserProfileState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileState.swift; sourceTree = "<group>"; };
F3203BE123CA31EB00265268 /* SampleData+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SampleData+AppStore.swift"; sourceTree = "<group>"; };
F3203BEA23CC977000265268 /* UserQRCodeContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserQRCodeContainerView.swift; sourceTree = "<group>"; };
F3203BEC23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserQRCodeContainerView+ViewModel.swift"; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -63,6 +84,7 @@
files = (
F3203BA223C7632C00265268 /* CypherPoetSwiftUIKit in Frameworks */,
F3203BA523C7634600265268 /* CypherPoetCoreDataKit in Frameworks */,
F3203BE523CA412600265268 /* Burritos in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -144,6 +166,8 @@
F3203B9B23C760C900265268 /* Reusables */ = {
isa = PBXGroup;
children = (
F3203BF223CD13F300265268 /* Protocols */,
F3203BD023C9DFDF00265268 /* ImageFilterService.swift */,
);
path = Reusables;
sourceTree = "<group>";
@ -159,6 +183,8 @@
F3203BA823C82C8F00265268 /* State */ = {
isa = PBXGroup;
children = (
F3203BD223C9F20C00265268 /* AppState.swift */,
F3203BD423C9F3E900265268 /* UserProfileState.swift */,
);
path = State;
sourceTree = "<group>";
@ -174,6 +200,11 @@
F3203BAC23C8337F00265268 /* User QR Code */ = {
isa = PBXGroup;
children = (
F3203BCE23C9DAE000265268 /* UserQRCodeView.swift */,
F3203BEA23CC977000265268 /* UserQRCodeContainerView.swift */,
F3203BEE23CC985100265268 /* UserQRCodeFormView.swift */,
F3203BEC23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift */,
F3203BF023CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift */,
);
path = "User QR Code";
sourceTree = "<group>";
@ -205,10 +236,19 @@
children = (
F3203BC423C8714700265268 /* SampleData.swift */,
F3203BC623C8717F00265268 /* SampleData+Contacts.swift */,
F3203BE123CA31EB00265268 /* SampleData+AppStore.swift */,
);
path = SampleData;
sourceTree = "<group>";
};
F3203BF223CD13F300265268 /* Protocols */ = {
isa = PBXGroup;
children = (
F3203BF323CD140100265268 /* ContactQRCodeRepresentable.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -228,6 +268,7 @@
packageProductDependencies = (
F3203BA123C7632C00265268 /* CypherPoetSwiftUIKit */,
F3203BA423C7634600265268 /* CypherPoetCoreDataKit */,
F3203BE423CA412600265268 /* Burritos */,
);
productName = QRConnections;
productReference = F3203B7E23C7609500265268 /* QRConnections.app */;
@ -260,6 +301,7 @@
packageReferences = (
F3203BA023C7632C00265268 /* XCRemoteSwiftPackageReference "CypherPoetSwiftUIKit" */,
F3203BA323C7634600265268 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */,
F3203BE323CA412500265268 /* XCRemoteSwiftPackageReference "Burritos" */,
);
productRefGroup = F3203B7F23C7609500265268 /* Products */;
projectDirPath = "";
@ -295,15 +337,25 @@
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 */,
F3203BB123C8345400265268 /* RootView.swift in Sources */,
F3203BC723C8717F00265268 /* SampleData+Contacts.swift in Sources */,
F3203BD323C9F20C00265268 /* AppState.swift in Sources */,
F3203BF423CD140100265268 /* ContactQRCodeRepresentable.swift in Sources */,
F3203BB723C844F100265268 /* ContactsListView.swift in Sources */,
F3203BED23CC97DC00265268 /* UserQRCodeContainerView+ViewModel.swift in Sources */,
F3203BB523C841A700265268 /* ContactsContainerView+ViewModel.swift in Sources */,
F3203BEB23CC977000265268 /* UserQRCodeContainerView.swift in Sources */,
F3203BF123CC9B1600265268 /* UserQRCodeFormView+ViewModel.swift in Sources */,
F3203BEF23CC985100265268 /* UserQRCodeFormView.swift in Sources */,
F3203BE223CA31EB00265268 /* SampleData+AppStore.swift in Sources */,
F3203BD123C9DFDF00265268 /* ImageFilterService.swift in Sources */,
F3203B8723C7609500265268 /* QRConnections.xcdatamodeld in Sources */,
F3203BBD23C8579400265268 /* Contact+CoreDataClass.swift in Sources */,
F3203BAA23C82CB100265268 /* CurrentApplication.swift in Sources */,
F3203BC523C8714700265268 /* SampleData.swift in Sources */,
F3203BD523C9F3E900265268 /* UserProfileState.swift in Sources */,
F3203B8423C7609500265268 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -516,6 +568,14 @@
minimumVersion = 0.0.3;
};
};
F3203BE323CA412500265268 /* XCRemoteSwiftPackageReference "Burritos" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/guillermomuntaner/Burritos.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -529,6 +589,11 @@
package = F3203BA323C7634600265268 /* XCRemoteSwiftPackageReference "CypherPoetCoreDataKit" */;
productName = CypherPoetCoreDataKit;
};
F3203BE423CA412600265268 /* Burritos */ = {
isa = XCSwiftPackageProductDependency;
package = F3203BE323CA412500265268 /* XCRemoteSwiftPackageReference "Burritos" */;
productName = Burritos;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "Burritos",
"repositoryURL": "https://github.com/guillermomuntaner/Burritos.git",
"state": {
"branch": null,
"revision": "309dbe1b5b3af8839ca6a7ebb2ad6ddf041bf420",
"version": "0.0.3"
}
},
{
"package": "CypherPoetCoreDataKit",
"repositoryURL": "https://github.com/CypherPoet/CypherPoetCoreDataKit.git",

View File

@ -12,9 +12,11 @@ import CypherPoetCoreDataKit_CoreDataManager
struct CurrentApplication {
var coreDataManager: CoreDataManager
var imageFilteringService: ImageFilterService
}
var CurrentApp = CurrentApplication(
coreDataManager: .shared
coreDataManager: .shared,
imageFilteringService: .shared
)

View File

@ -25,6 +25,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let appStore = AppStore(initialState: AppState(), appReducer: appReducer)
// Get the managed object context from the shared persistent container.
let managedObjectContext = CurrentApp.coreDataManager.mainContext
@ -37,6 +38,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
let contentView = RootView()
.environment(\.managedObjectContext, managedObjectContext)
.environmentObject(appStore)
window.rootViewController = UIHostingController(rootView: contentView)

View File

@ -11,15 +11,18 @@ import Foundation
import CoreData
extension Contact {
@NSManaged public var uuid: UUID?
@NSManaged public var name: String?
extension Contact: ContactQRCodeRepresentable {
@NSManaged public var qrCodeData: Data?
@NSManaged public var statusValue: Int16
var status: Status {
get { Contact.Status(rawValue: statusValue)! }
set { statusValue = newValue.rawValue }
}
// MARK: - ContactQRCodeRepresentable
@NSManaged public var name: String
@NSManaged public var uuid: UUID
}

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Contact" representedClassName="Contact" syncable="YES">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="qrCodeData" optional="YES" attributeType="Binary"/>
<attribute name="statusValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uuid" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="uuid" attributeType="UUID" usesScalarValueType="NO"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="uuid"/>
@ -11,6 +12,6 @@
</uniquenessConstraints>
</entity>
<elements>
<element name="Contact" positionX="-63" positionY="-18" width="128" height="88"/>
<element name="Contact" positionX="6.3984375" positionY="-108.1640625" width="128" height="103"/>
</elements>
</model>

View File

@ -0,0 +1,38 @@
//
// AppState.swift
// QRConnections
//
// Created by CypherPoet on 1/11/20.
//
//
import Foundation
import CypherPoetSwiftUIKit_DataFlowUtils
struct AppState {
var userProfileState = UserProfileState()
}
//enum AppSideEffect: SideEffect {}
enum AppAction {
case userProfile(_ action: UserProfileAction)
}
// MARK: - Reducer
let appReducer = Reducer<AppState, AppAction> { appState, action in
switch action {
case .userProfile(let action):
userProfileReducer.reduce(&appState.userProfileState, action)
}
}
typealias AppStore = Store<AppState, AppAction>

View File

@ -0,0 +1,92 @@
//
// UserProfileState.swift
// QRConnections
//
// Created by CypherPoet on 1/11/20.
//
//
import CypherPoetSwiftUIKit_DataFlowUtils
import UserDefault
import Combine
import CoreImage.CIFilterBuiltins
import UIKit
struct UserProfileState: ContactQRCodeRepresentable {
@UserDefault("user-profile-qr-code-data", defaultValue: Data())
var qrCodeData: Data
@UserDefault("user-profile-uuid-string", defaultValue: UUID().uuidString)
var uuidString: String
@UserDefault("user-profile-name", defaultValue: "")
var name: String
var uuid: UUID {
guard let uuid = UUID(uuidString: uuidString) else {
preconditionFailure("Unable to make UUID from stored `uuidString`")
}
return uuid
}
var qrCodeCGImage: CGImage? {
didSet {
guard let cgImage = qrCodeCGImage else { return }
self.qrCodeData = UIImage(cgImage: cgImage).pngData() ?? Data()
}
}
}
enum UserProfileSideEffect: SideEffect {
case generateQRCode(data: Data)
func mapToAction() -> AnyPublisher<AppAction, Never> {
switch self {
case .generateQRCode(let data):
return Just(data)
.flatMap { data in
CurrentApp.imageFilteringService.createCGImage(
from: CIFilter.qrCodeGenerator(),
withAttributes: [
"inputMessage": data
]
)
.map { AppAction.userProfile(.qrCodeImageSet($0)) }
.catch { _ in Just(AppAction.userProfile(.qrCodeDataSet(Data()))) }
}
.eraseToAnyPublisher()
}
}
}
enum UserProfileAction {
case qrCodeImageSet(CGImage)
case qrCodeDataSet(Data)
case userNameSet(String)
}
// MARK: - Reducer
let userProfileReducer: Reducer<UserProfileState, UserProfileAction> = Reducer(
reduce: { state, action in
switch action {
case .qrCodeDataSet(let data):
state.qrCodeData = data
case .qrCodeImageSet(let cgImage):
state.qrCodeCGImage = cgImage
case .userNameSet(let name):
state.name = name
}
}
)

View File

@ -0,0 +1,23 @@
//
// SampleData+AppStore.swift
// QRConnections
//
// Created by CypherPoet on 1/11/20.
//
//
import Foundation
extension SampleData {
static let userProfileState = UserProfileState()
static let appStore = AppStore(
initialState: AppState(
userProfileState: SampleData.userProfileState
),
appReducer: appReducer
)
}

View File

@ -11,7 +11,6 @@ import CoreData
extension SampleData {
static let contactUUIDs: [UUID] = [
UUID(uuidString: "879da3b9-4f3b-459f-8b53-2e39908e1900")!,
UUID(uuidString: "23bb3482-3655-4292-ae90-223c3ac6fa69")!,
@ -19,14 +18,26 @@ extension SampleData {
UUID(uuidString: "c3edfa5c-3278-4f45-b86e-c272ba787a70")!,
UUID(uuidString: "611132c1-2276-435d-b7fd-5e1f54e6f203")!,
]
// static let contactUUIDStrings: [UUID] = [
// "879da3b9-4f3b-459f-8b53-2e39908e1900",
// "23bb3482-3655-4292-ae90-223c3ac6fa69",
// "015505c4-2732-48e3-a697-0c45ec1a4ccf",
// "c3edfa5c-3278-4f45-b86e-c272ba787a70",
// "611132c1-2276-435d-b7fd-5e1f54e6f203",
// ]
static func makeContacts(in context: NSManagedObjectContext) -> [Contact] {
contactUUIDs.enumerated().map { (index, uuid) in
let contact = Contact(context: context)
// let contactInfo = ContactInfo(context: context)
contact.status = Contact.Status.allCases.randomElement()!
contact.uuid = uuid
contact.name = "Contact \(index + 1)"
contact.status = Contact.Status.allCases.randomElement()!
// contactInfo.uuid = uuid
// contactInfo.name = "Contact \(index + 1)"
// contactInfo.contact = contact
return contact
}

View File

@ -0,0 +1,92 @@
import Foundation
import CoreImage
import CoreImage.CIFilterBuiltins
import Combine
import UIKit
public final class ImageFilterService {
public typealias FilterAtrributes = [String: Any]
public enum Error: Swift.Error {
case cgImage(_ message: String)
case ciImage(_ message: String)
case filtering(_ message: String)
}
// 🔑 Creating a CIContext is expensive, so we'll create it once and reuse it throughout the app.
lazy private var context = CIContext()
public let filteringQueue: DispatchQueue
public init(
queue filteringQueue: DispatchQueue = DispatchQueue(
label: "Image Filtering Service",
qos: .userInitiated
)
) {
self.filteringQueue = filteringQueue
}
}
extension ImageFilterService {
public func createCGImage(
from filter: CIFilter,
withAttributes filterAtrributes: FilterAtrributes = [:]
) -> AnyPublisher<CGImage, ImageFilterService.Error> {
createCIImage(byApplying: filter, withAttributes: filterAtrributes)
.flatMap(createCGImage(from:))
.eraseToAnyPublisher()
}
private func createCGImage(
from filteredImage: CIImage
) -> AnyPublisher<CGImage, ImageFilterService.Error> {
Just(filteredImage)
.print("createCGImage")
.tryMap { filteredImage -> CGImage in
guard
let cgImage = self.context.createCGImage(filteredImage, from: filteredImage.extent)
else {
print("createCGImage failed")
throw Error.cgImage("Failed to create cgImage from filtered ciImage")
}
return cgImage
}
.mapError( { $0 as! ImageFilterService.Error })
.eraseToAnyPublisher()
}
private func createCIImage(
byApplying filter: CIFilter,
withAttributes filterAtrributes: FilterAtrributes = [:]
) -> AnyPublisher<CIImage, ImageFilterService.Error> {
Just(filter)
.tryMap { filter in
for (key, value) in filterAtrributes {
filter.setValue(value, forKey: key)
}
guard let filteredImage = filter.outputImage else {
throw Error.filtering("Failed to output image during filtering")
}
return filteredImage
}
.mapError( { $0 as! ImageFilterService.Error })
.eraseToAnyPublisher()
}
}
extension ImageFilterService {
public static let shared = ImageFilterService()
}

View File

@ -0,0 +1,15 @@
//
// ContactQRCodeRepresentable.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import Foundation
public protocol ContactQRCodeRepresentable {
var uuid: UUID { get }
var name: String { get }
}

View File

@ -10,7 +10,7 @@ import SwiftUI
struct ContactsContainerView: View {
@ObservedObject var viewModel: ViewModel
@ObservedObject var viewModel = ViewModel()
}

View File

@ -25,7 +25,7 @@ extension ContactsListView {
var body: some View {
List(contacts) { contact in
Text(contact.name ?? "No Name")
Text(contact.name)
}
}
}

View File

@ -10,6 +10,8 @@ import SwiftUI
struct RootView: View {
@EnvironmentObject private var store: AppStore
enum Tab {
case collectedContacts
case userQRCode
@ -32,13 +34,15 @@ extension RootView {
}
.tag(Tab.collectedContacts)
Text("")
UserQRCodeContainerView(viewModel: .init(userProfileState: userProfileState))
.tabItem {
Image(systemName: "qrcode")
Text("My Alias")
Text("Your QR")
}
.tag(Tab.userQRCode)
}
// .environmentObject(store)
.edgesIgnoringSafeArea(.top)
}
}
@ -46,8 +50,7 @@ extension RootView {
// MARK: - Computeds
extension RootView {
var userProfileState: UserProfileState { store.state.userProfileState }
}
@ -64,5 +67,6 @@ struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
.environmentObject(SampleData.appStore)
}
}

View File

@ -0,0 +1,77 @@
//
// UserQRCodeContainerView+ViewModel.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import SwiftUI
import Combine
extension UserQRCodeContainerView {
final class ViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>()
var userProfileState: UserProfileState
// MARK: - Published Outputs
@Published var qrCodeCGImage: CGImage?
// MARK: - Init
init(
userProfileState: UserProfileState
) {
self.userProfileState = userProfileState
self.qrCodeCGImage = userProfileState.qrCodeCGImage
setupSubscribers()
}
}
}
// MARK: - Publishers
extension UserQRCodeContainerView.ViewModel {
private var userProfileStatePublisher: Publishers.Share<AnyPublisher<UserProfileState, Never>> {
CurrentValueSubject(userProfileState)
.eraseToAnyPublisher()
.share()
}
private var qrCodeCGImagePublisher: AnyPublisher<CGImage?, Never> {
userProfileStatePublisher
.map(\.qrCodeCGImage)
.eraseToAnyPublisher()
}
}
// MARK: - Computeds
extension UserQRCodeContainerView.ViewModel {
}
// MARK: - Public Methods
extension UserQRCodeContainerView.ViewModel {
}
// MARK: - Private Helpers
private extension UserQRCodeContainerView.ViewModel {
func setupSubscribers() {
qrCodeCGImagePublisher
.receive(on: DispatchQueue.main)
.assign(to: \.qrCodeCGImage, on: self)
.store(in: &subscriptions)
}
}

View File

@ -0,0 +1,79 @@
//
// UserQRCodeContainerView.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import SwiftUI
struct UserQRCodeContainerView {
@EnvironmentObject private var store: AppStore
@ObservedObject var viewModel: ViewModel
}
// MARK: - View
extension UserQRCodeContainerView: View {
var body: some View {
NavigationView {
VStack {
if viewModel.qrCodeCGImage != nil {
UserQRCodeView(cgImage: viewModel.qrCodeCGImage!)
.transition(
AnyTransition
.move(edge: .top)
.animation(Animation.easeOut(duration: 0.4))
)
}
UserQRCodeFormView(
viewModel: .init(userProfileState: viewModel.userProfileState)
)
}
.navigationBarTitle("Your QR")
}
.onAppear {
let qrCodeData = self.viewModel.userProfileState.qrCodeData
if self.viewModel.qrCodeCGImage == nil && !qrCodeData.isEmpty {
self.store.send(UserProfileSideEffect.generateQRCode(data: qrCodeData))
}
}
}
}
// MARK: - Computeds
extension UserQRCodeContainerView {
}
// MARK: - View Variables
extension UserQRCodeContainerView {
}
// MARK: - Private Helpers
private extension UserQRCodeContainerView {
}
// MARK: - Preview
struct UserQRCodeContainerView_Previews: PreviewProvider {
static var previews: some View {
let store = SampleData.appStore
return UserQRCodeContainerView(
viewModel: .init(userProfileState: store.state.userProfileState)
)
.environmentObject(store)
}
}

View File

@ -0,0 +1,103 @@
//
// UserQRCodeFormView+ViewModel.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import SwiftUI
import Combine
extension UserQRCodeFormView {
final class ViewModel: ObservableObject {
private var subscriptions = Set<AnyCancellable>()
private var userProfileState: UserProfileState
// MARK: - Form Inputs
@Published var userNameText: String = ""
// MARK: - Published Outputs
@Published var userName: String = ""
@Published var qrCodeData = Data()
// MARK: - Init
init(
userProfileState: UserProfileState
) {
self.userProfileState = userProfileState
self.userNameText = userProfileState.name
setupSubscribers()
}
}
}
// MARK: - Publishers
extension UserQRCodeFormView.ViewModel {
private var userProfileStatePublisher: Publishers.Share<AnyPublisher<UserProfileState, Never>> {
CurrentValueSubject(userProfileState)
.eraseToAnyPublisher()
.share()
}
private var userNameTextPublisher: AnyPublisher<String, Never> {
$userNameText
.dropFirst(1)
.debounce(for: .milliseconds(650), scheduler: DispatchQueue.main)
.removeDuplicates()
.eraseToAnyPublisher()
}
private var userNamePublisher: Publishers.Share<AnyPublisher<String, Never>> {
userNameTextPublisher
.share()
}
private var qrCodeDataPublisher: AnyPublisher<Data, Never> {
userNamePublisher.share()
// .print("qrCodeDataPublisher")
.map { text in
text
.appending(self.userProfileState.uuid.uuidString)
.data(using: .utf8)!
}
.eraseToAnyPublisher()
}
}
// MARK: - Computeds
extension UserQRCodeFormView.ViewModel {}
// MARK: - Public Methods
extension UserQRCodeFormView.ViewModel {}
// MARK: - Private Helpers
private extension UserQRCodeFormView.ViewModel {
func setupSubscribers() {
qrCodeDataPublisher
.receive(on: DispatchQueue.main)
.assign(to: \.qrCodeData, on: self)
.store(in: &subscriptions)
userNamePublisher
.receive(on: DispatchQueue.main)
.print("userNamePublisher - assigning")
.assign(to: \.userName, on: self)
.store(in: &subscriptions)
}
}

View File

@ -0,0 +1,55 @@
//
// UserQRCodeFormView.swift
// QRConnections
//
// Created by CypherPoet on 1/13/20.
//
//
import SwiftUI
struct UserQRCodeFormView: View {
@EnvironmentObject private var store: AppStore
@ObservedObject var viewModel: ViewModel
}
// MARK: - Body
extension UserQRCodeFormView {
var body: some View {
Form {
Section {
TextField("Name", text: $viewModel.userNameText)
}
}
.onReceive(viewModel.$userName.dropFirst(1)) { name in
self.store.send(.userProfile(.userNameSet(name)))
}
.onReceive(viewModel.$qrCodeData.dropFirst(1)) { data in
self.store.send(UserProfileSideEffect.generateQRCode(data: data))
}
}
}
// MARK: - Computeds
extension UserQRCodeFormView {}
// MARK: - View Variables
extension UserQRCodeFormView {}
// MARK: - Preview
struct UserQRCodeFormView_Previews: PreviewProvider {
static var previews: some View {
let store = SampleData.appStore
return UserQRCodeFormView(viewModel: .init(userProfileState: store.state.userProfileState))
.environmentObject(store)
}
}

View File

@ -0,0 +1,47 @@
//
// UserQRCodeView.swift
// QRConnections
//
// Created by CypherPoet on 1/11/20.
//
//
import SwiftUI
struct UserQRCodeView {
let cgImage: CGImage
}
// MARK: - View
extension UserQRCodeView: View {
var body: some View {
Image(uiImage: .init(cgImage: cgImage))
}
}
// MARK: - Computeds
extension UserQRCodeView {}
// MARK: - View Variables
extension UserQRCodeView {}
// MARK: - Private Helpers
private extension UserQRCodeView {
}
//// MARK: - Preview
//struct UserQRCodeView_Previews: PreviewProvider {
//
// static var previews: some View {
// UserQRCodeView(cgImage: )
// }
//}