From 5a96727b014c500606b26c73a5e7dc93e611d764 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Sat, 17 Feb 2024 12:04:55 +0100 Subject: [PATCH] [#988] Scan UI - new layout for the scan screen [#988] Scan UI - new layout and design for scan - camera not authorized case + UI - scan store refactored to the latest TCA - unit test fixed [#988] Scan UI (#1069) - changelog update --- CHANGELOG.md | 1 + .../xcshareddata/xcschemes/Scan.xcscheme | 66 +++++ .../CaptureDeviceInterface.swift | 10 +- .../CaptureDevice/CaptureDeviceLiveKey.swift | 9 +- .../CaptureDevice/CaptureDeviceTestKey.swift | 2 + modules/Sources/Features/Home/HomeStore.swift | 4 +- .../Features/Sandbox/SandboxView.swift | 2 +- modules/Sources/Features/Scan/ScanStore.swift | 134 +++------- modules/Sources/Features/Scan/ScanView.swift | 239 +++++++++++------- .../Features/SendFlow/SendFlowStore.swift | 14 +- .../ServerSetup/ServerSetupView.swift | 9 +- modules/Sources/Generated/L10n.swift | 18 +- .../torchOff.imageset/Contents.json | 12 + .../torchOff.imageset/torchOff.png | Bin 0 -> 9846 bytes .../torchOn.imageset/Contents.json | 12 + .../torchOn.imageset/torchOn.png | Bin 0 -> 9128 bytes .../Generated/Resources/Localizable.strings | 6 +- .../Generated/XCAssets+Generated.swift | 2 + .../UIComponents/Buttons/ZashiButton.swift | 2 +- secantTests/ScanTests/ScanTests.swift | 55 ++-- 20 files changed, 337 insertions(+), 260 deletions(-) create mode 100644 modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme create mode 100644 modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json create mode 100644 modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png create mode 100644 modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json create mode 100644 modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8fc6f..60114cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ directly impact users rather than highlighting other crucial architectural updat ### Changed - Settings screen options have been reduced and some were moved to the new Advanced Settings screen. +- Scan of QR codes has been re-worked with new design and behaviours. ### Added - Pending values (changes) at the Balances tab. diff --git a/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme b/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme new file mode 100644 index 0000000..4a16b67 --- /dev/null +++ b/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift index 3e1ed8c..e1c001e 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift @@ -16,11 +16,13 @@ extension DependencyValues { public struct CaptureDeviceClient { public enum CaptureDeviceClientError: Error { - case captureDeviceFailed - case lockForConfigurationFailed + case authorizationStatus + case captureDevice + case lockForConfiguration case torchUnavailable } - - public let isTorchAvailable: () throws -> Bool + + public let isAuthorized: () -> Bool + public let isTorchAvailable: () -> Bool public let torch: (Bool) throws -> Void } diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift index d9e0be3..e44474e 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift @@ -10,16 +10,19 @@ import ComposableArchitecture extension CaptureDeviceClient: DependencyKey { public static let liveValue = Self( + isAuthorized: { + AVCaptureDevice.authorizationStatus(for: .video) == .authorized + }, isTorchAvailable: { guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { - throw CaptureDeviceClientError.captureDeviceFailed + return false } return videoCaptureDevice.hasTorch }, torch: { isTorchOn in guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { - throw CaptureDeviceClientError.captureDeviceFailed + throw CaptureDeviceClientError.captureDevice } guard videoCaptureDevice.hasTorch else { @@ -31,7 +34,7 @@ extension CaptureDeviceClient: DependencyKey { videoCaptureDevice.torchMode = isTorchOn ? .on : .off videoCaptureDevice.unlockForConfiguration() } catch { - throw CaptureDeviceClientError.lockForConfigurationFailed + throw CaptureDeviceClientError.lockForConfiguration } } ) diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift index 00ddf28..893d4fb 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift @@ -10,6 +10,7 @@ import XCTestDynamicOverlay extension CaptureDeviceClient: TestDependencyKey { public static let testValue = Self( + isAuthorized: XCTUnimplemented("\(Self.self).isAuthorized", placeholder: false), isTorchAvailable: XCTUnimplemented("\(Self.self).isTorchAvailable", placeholder: false), torch: XCTUnimplemented("\(Self.self).torch") ) @@ -17,6 +18,7 @@ extension CaptureDeviceClient: TestDependencyKey { extension CaptureDeviceClient { public static let noOp = Self( + isAuthorized: { false }, isTorchAvailable: { false }, torch: { _ in } ) diff --git a/modules/Sources/Features/Home/HomeStore.swift b/modules/Sources/Features/Home/HomeStore.swift index 252b65c..eb63473 100644 --- a/modules/Sources/Features/Home/HomeStore.swift +++ b/modules/Sources/Features/Home/HomeStore.swift @@ -31,7 +31,7 @@ public struct HomeReducer: Reducer { public var canRequestReview = false public var isRestoringWallet = false public var requiredTransactionConfirmations = 0 - public var scanState: ScanReducer.State + public var scanState: Scan.State public var shieldedBalance: Zatoshi public var synchronizerStatusSnapshot: SyncStatusSnapshot public var syncProgressState: SyncProgressReducer.State @@ -55,7 +55,7 @@ public struct HomeReducer: Reducer { canRequestReview: Bool = false, isRestoringWallet: Bool = false, requiredTransactionConfirmations: Int = 0, - scanState: ScanReducer.State, + scanState: Scan.State, shieldedBalance: Zatoshi, synchronizerStatusSnapshot: SyncStatusSnapshot, syncProgressState: SyncProgressReducer.State, diff --git a/modules/Sources/Features/Sandbox/SandboxView.swift b/modules/Sources/Features/Sandbox/SandboxView.swift index 0d12563..29997d0 100644 --- a/modules/Sources/Features/Sandbox/SandboxView.swift +++ b/modules/Sources/Features/Sandbox/SandboxView.swift @@ -42,7 +42,7 @@ public struct SandboxView: View { case .recoveryPhraseDisplay: RecoveryPhraseDisplayView(store: RecoveryPhraseDisplay.placeholder) case .scan: - ScanView(store: .placeholder) + ScanView(store: Scan.placeholder) } } diff --git a/modules/Sources/Features/Scan/ScanStore.swift b/modules/Sources/Features/Scan/ScanStore.swift index 038d6b9..cbf91f1 100644 --- a/modules/Sources/Features/Scan/ScanStore.swift +++ b/modules/Sources/Features/Scan/ScanStore.swift @@ -1,5 +1,5 @@ // -// ScanUIView.swift +// Scan.swift // secant-testnet // // Created by Lukáš Korba on 16.05.2022. @@ -14,47 +14,24 @@ import ZcashLightClientKit import Generated import ZcashSDKEnvironment -public typealias ScanStore = Store -public typealias ScanViewStore = ViewStore - -public struct ScanReducer: Reducer { +@Reducer +public struct Scan { private enum CancelId { case timer } + @ObservableState public struct State: Equatable { - public enum ScanStatus: Equatable { - case failed - case value(RedactableString) - case unknown - } - - @PresentationState public var alert: AlertState? + public var info = "" public var isTorchAvailable = false public var isTorchOn = false - public var scanStatus: ScanStatus = .unknown - public var scannedValue: String? { - guard case let .value(scannedValue) = scanStatus else { - return nil - } - - return scannedValue.data - } - - public var isValidValue: Bool { - if case .value = scanStatus { - return true - } - return false - } - public init( + info: String = "", isTorchAvailable: Bool = false, - isTorchOn: Bool = false, - scanStatus: ScanStatus = .unknown + isTorchOn: Bool = false ) { + self.info = info self.isTorchAvailable = isTorchAvailable self.isTorchOn = isTorchOn - self.scanStatus = scanStatus } } @@ -64,7 +41,8 @@ public struct ScanReducer: Reducer { @Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment public enum Action: Equatable { - case alert(PresentationAction) + case cancelPressed + case clearInfo case onAppear case onDisappear case found(RedactableString) @@ -79,98 +57,54 @@ public struct ScanReducer: Reducer { public var body: some ReducerOf { Reduce { state, action in switch action { - case .alert(.presented(let action)): - return Effect.send(action) - - case .alert(.dismiss): - state.alert = nil - return .none - - case .alert: - return .none - case .onAppear: // reset the values - state.scanStatus = .unknown state.isTorchOn = false // check the torch availability - do { - state.isTorchAvailable = try captureDevice.isTorchAvailable() - } catch { - state.alert = AlertState.cantInitializeCamera(error.toZcashError()) + state.isTorchAvailable = captureDevice.isTorchAvailable() + if !captureDevice.isAuthorized() { + state.info = L10n.Scan.cameraSettings } return .none case .onDisappear: return .cancel(id: CancelId.timer) + case .cancelPressed: + return .none + + case .clearInfo: + state.info = "" + return .cancel(id: CancelId.timer) + case .found: return .none - + case .scanFailed: - state.scanStatus = .failed - return .none - + state.info = L10n.Scan.invalidQR + return .concatenate( + Effect.cancel(id: CancelId.timer), + .run { send in + try await mainQueue.sleep(for: .seconds(3)) + await send(.clearInfo) + } + .cancellable(id: CancelId.timer, cancelInFlight: true) + ) + case .scan(let code): - // the logic for the same scanned code is skipped until some new code - if let prevCode = state.scannedValue, prevCode == code.data { - return .none - } if uriParser.isValidURI(code.data, zcashSDKEnvironment.network.networkType) { - state.scanStatus = .value(code) - // once valid URI is scanned we want to start the timer to deliver the code - // any new code cancels the schedule and fires new one - return .concatenate( - Effect.cancel(id: CancelId.timer), - .run { send in - try await mainQueue.sleep(for: .seconds(1)) - await send(.found(code)) - } - .cancellable(id: CancelId.timer, cancelInFlight: true) - ) + return .send(.found(code)) } else { - state.scanStatus = .failed + return .send(.scanFailed) } - return .cancel(id: CancelId.timer) case .torchPressed: do { try captureDevice.torch(!state.isTorchOn) state.isTorchOn.toggle() - } catch { - state.alert = AlertState.cantInitializeCamera(error.toZcashError()) - } + } catch { } return .none } } - .ifLet(\.$alert, action: /Action.alert) - } -} - -// MARK: Alerts - -extension AlertState where Action == ScanReducer.Action { - public static func cantInitializeCamera(_ error: ZcashError) -> AlertState { - AlertState { - TextState(L10n.Scan.Alert.CantInitializeCamera.title) - } message: { - TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.message, error.code.rawValue)) - } - } -} - -// MARK: Placeholders - -extension ScanReducer.State { - public static var initial: Self { - .init() - } -} - -extension ScanStore { - public static let placeholder = ScanStore( - initialState: .initial - ) { - ScanReducer() } } diff --git a/modules/Sources/Features/Scan/ScanView.swift b/modules/Sources/Features/Scan/ScanView.swift index 332c849..99db94a 100644 --- a/modules/Sources/Features/Scan/ScanView.swift +++ b/modules/Sources/Features/Scan/ScanView.swift @@ -11,134 +11,165 @@ import Generated import UIComponents public struct ScanView: View { - @Environment(\.presentationMode) var presentationMode + let store: StoreOf - let store: ScanStore - - public init(store: ScanStore) { + + public init(store: StoreOf) { self.store = store } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - GeometryReader { proxy in - ZStack { + WithPerceptionTracking { + ZStack { + GeometryReader { proxy in QRCodeScanView( rectOfInterest: normalizedRectOfInterest(proxy.size), - onQRScanningDidFail: { viewStore.send(.scanFailed) }, - onQRScanningSucceededWithCode: { viewStore.send(.scan($0.redacted)) } + onQRScanningDidFail: { store.send(.scanFailed) }, + onQRScanningSucceededWithCode: { store.send(.scan($0.redacted)) } ) - backButton - - if viewStore.isTorchAvailable { - torchButton(viewStore) - } - frameOfInterest(proxy.size) - VStack { - Spacer() - - Text(L10n.Scan.info) - .padding(.bottom, 10) - - if let scannedValue = viewStore.scannedValue { - Text(scannedValue) - .foregroundColor(viewStore.isValidValue ? .green : .red) - } else { - Text(L10n.Scan.scanning) - } + if store.isTorchAvailable { + torchButton(store, size: proxy.size) } - .padding() } - .navigationBarHidden(true) - .applyScreenBackground() - .onAppear { viewStore.send(.onAppear) } - .onDisappear { viewStore.send(.onDisappear) } + + VStack { + Spacer() + + Text(store.info) + .font(Font.custom("Inter", size: 14)) + .foregroundColor(Asset.Colors.secondary.color) + .multilineTextAlignment(.center) + .padding(.bottom, 20) + + Button(L10n.General.cancel.uppercased()) { + store.send(.cancelPressed) + } + .zcashStyle(.secondary) + .padding(.horizontal, 50) + .padding(.bottom, 70) + } + .padding(.horizontal, 30) } + .edgesIgnoringSafeArea(.all) .ignoresSafeArea() + .navigationBarHidden(true) + .applyScreenBackground() + .onAppear { store.send(.onAppear) } + .onDisappear { store.send(.onDisappear) } } - .alert(store: store.scope( - state: \.$alert, - action: { .alert($0) } - )) } } extension ScanView { - var backButton: some View { - VStack { - HStack { - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Image(systemName: "arrow.backward") - .foregroundColor(Asset.Colors.primary.color) - .font( - .custom(FontFamily.Inter.regular.name, size: 30) - ) - }) - .padding(.top, 10) - - Spacer() + func torchButton(_ store: StoreOf, size: CGSize) -> some View { + let center = ScanView.rectOfInterest(size).origin + let frameHalfSize = ScanView.frameSize(size) * 0.5 + + return Button { + store.send(.torchPressed) + } label: { + if store.isTorchOn { + Asset.Assets.torchOff.image + .renderingMode(.template) + .resizable() + .frame(width: 20, height: 20) + .tint(.white) + } else { + Asset.Assets.torchOn.image + .renderingMode(.template) + .resizable() + .frame(width: 20, height: 20) + .tint(.white) } - .padding() - - Spacer() } - .padding() - } - - func torchButton(_ viewStore: ScanViewStore) -> some View { - VStack { - HStack { - Spacer() - - Button( - action: { viewStore.send(.torchPressed) }, - label: { - Image( - systemName: viewStore.isTorchOn ? "lightbulb.fill" : "lightbulb.slash" - ) - .foregroundColor(Asset.Colors.primary.color) - .font( - .custom(FontFamily.Inter.regular.name, size: 30) - ) - } - ) - .padding(.top, 10) - } - .padding() - - Spacer() - } - .padding() + .position( + x: center.x + frameHalfSize - 5, + y: center.y + frameHalfSize + 20 + ) } func frameOfInterest(_ size: CGSize) -> some View { - Rectangle() - .stroke(Asset.Colors.primary.color, lineWidth: 5.0) - .frame( - width: frameSize(size), - height: frameSize(size), - alignment: .center - ) - .edgesIgnoringSafeArea(.all) - .ignoresSafeArea() - .position( - x: rectOfInterest(size).origin.x, - y: rectOfInterest(size).origin.y - ) + let center = ScanView.rectOfInterest(size).origin + let frameSize = ScanView.frameSize(size) + let halfSize = frameSize * 0.5 + let cornersLength = 36.0 + let cornersHalfLength = cornersLength * 0.5 + let leadMarkColor = Color.white + + return ZStack { + Color.black + .opacity(0.65) + .edgesIgnoringSafeArea(.all) + .ignoresSafeArea() + .reverseMask { + Rectangle() + .frame( + width: frameSize, + height: frameSize, + alignment: .center + ) + .position( + x: center.x, + y: center.y + ) + } + + // horizontal lead marks + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x - halfSize + cornersHalfLength, y: center.y - halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x + halfSize - cornersHalfLength, y: center.y - halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x - halfSize + cornersHalfLength, y: center.y + halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x + halfSize - cornersHalfLength, y: center.y + halfSize) + + // vertical lead marks + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x - halfSize, y: center.y - halfSize + cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x - halfSize, y: center.y + halfSize - cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x + halfSize, y: center.y - halfSize + cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x + halfSize, y: center.y + halfSize - cornersHalfLength) + } + } +} + +extension View { + @inlinable + public func reverseMask( + alignment: Alignment = .center, + @ViewBuilder _ mask: () -> Mask + ) -> some View { + self.mask { + Rectangle() + .overlay(alignment: alignment) { + mask() + .blendMode(.destinationOut) + } + } } } extension ScanView { - func frameSize(_ size: CGSize) -> CGFloat { + static func frameSize(_ size: CGSize) -> CGFloat { size.width * 0.55 } - func rectOfInterest(_ size: CGSize) -> CGRect { + static func rectOfInterest(_ size: CGSize) -> CGRect { CGRect( x: size.width * 0.5, y: size.height * 0.5, @@ -161,6 +192,20 @@ extension ScanView { struct ScanView_Previews: PreviewProvider { static var previews: some View { - ScanView(store: .placeholder) + ScanView(store: Scan.placeholder) + } +} + +// MARK: Placeholders + +extension Scan.State { + public static var initial = Scan.State() +} + +extension Scan { + public static let placeholder = StoreOf( + initialState: .initial + ) { + Scan() } } diff --git a/modules/Sources/Features/SendFlow/SendFlowStore.swift b/modules/Sources/Features/SendFlow/SendFlowStore.swift index e8290fe..c0fe1a3 100644 --- a/modules/Sources/Features/SendFlow/SendFlowStore.swift +++ b/modules/Sources/Features/SendFlow/SendFlowStore.swift @@ -37,7 +37,7 @@ public struct SendFlowReducer: Reducer { public var destination: Destination? public var isSending = false public var memoState: MessageEditorReducer.State - public var scanState: ScanReducer.State + public var scanState: Scan.State public var shieldedBalance = Zatoshi.zero public var totalBalance = Zatoshi.zero public var transactionAddressInputState: TransactionAddressTextFieldReducer.State @@ -106,7 +106,7 @@ public struct SendFlowReducer: Reducer { addMemoState: Bool, destination: Destination? = nil, memoState: MessageEditorReducer.State, - scanState: ScanReducer.State, + scanState: Scan.State, shieldedBalance: Zatoshi = .zero, totalBalance: Zatoshi = .zero, transactionAddressInputState: TransactionAddressTextFieldReducer.State, @@ -130,7 +130,7 @@ public struct SendFlowReducer: Reducer { case onAppear case onDisappear case reviewPressed - case scan(ScanReducer.Action) + case scan(Scan.Action) case sendPressed case sendDone(TransactionState) case sendFailed(ZcashError) @@ -164,7 +164,7 @@ public struct SendFlowReducer: Reducer { } Scope(state: \.scanState, action: /Action.scan) { - ScanReducer() + Scan() } Reduce { state, action in @@ -283,6 +283,10 @@ public struct SendFlowReducer: Reducer { audioServices.systemSoundVibrate() return Effect.send(.updateDestination(nil)) + case .scan(.cancelPressed): + state.destination = nil + return .none + case .scan: return .none } @@ -312,7 +316,7 @@ extension SendFlowStore { ) } - func scanStore() -> ScanStore { + func scanStore() -> StoreOf { self.scope( state: \.scanState, action: SendFlowReducer.Action.scan diff --git a/modules/Sources/Features/ServerSetup/ServerSetupView.swift b/modules/Sources/Features/ServerSetup/ServerSetupView.swift index d028206..fb1a4be 100644 --- a/modules/Sources/Features/ServerSetup/ServerSetupView.swift +++ b/modules/Sources/Features/ServerSetup/ServerSetupView.swift @@ -111,19 +111,14 @@ public struct ServerSetupView: View { #Preview { ServerSetupView( - store: .init( - initialState: - ServerSetup.State(server: .custom) - ) { - ServerSetup() - } + store: ServerSetup.placeholder ) } // MARK: Placeholders extension ServerSetup.State { - public static let initial = ServerSetup.State() + public static var initial = ServerSetup.State() } extension ServerSetup { diff --git a/modules/Sources/Generated/L10n.swift b/modules/Sources/Generated/L10n.swift index 8d40ad8..dc1ff4a 100644 --- a/modules/Sources/Generated/L10n.swift +++ b/modules/Sources/Generated/L10n.swift @@ -477,20 +477,10 @@ public enum L10n { } } public enum Scan { - /// We will validate any Zcash URI and take you to the appropriate action. - public static let info = L10n.tr("Localizable", "scan.info", fallback: "We will validate any Zcash URI and take you to the appropriate action.") - /// Scanning... - public static let scanning = L10n.tr("Localizable", "scan.scanning", fallback: "Scanning...") - public enum Alert { - public enum CantInitializeCamera { - /// Error: %@ (code: %@) - public static func message(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "scan.alert.cantInitializeCamera.message", String(describing: p1), String(describing: p2), fallback: "Error: %@ (code: %@)") - } - /// Can't initialize the camera - public static let title = L10n.tr("Localizable", "scan.alert.cantInitializeCamera.title", fallback: "Can't initialize the camera") - } - } + /// The camera is not authorized. Please go to the system settings of Zashi and turn it on. + public static let cameraSettings = L10n.tr("Localizable", "scan.cameraSettings", fallback: "The camera is not authorized. Please go to the system settings of Zashi and turn it on.") + /// This QR code doesn't hold a valid Zcash address. + public static let invalidQR = L10n.tr("Localizable", "scan.invalidQR", fallback: "This QR code doesn't hold a valid Zcash address.") } public enum SecurityWarning { /// I acknowledge diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json new file mode 100644 index 0000000..1db5e99 --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "torchOff.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png new file mode 100644 index 0000000000000000000000000000000000000000..0897761ebd8b0936132e49758c68081a3f5ba886 GIT binary patch literal 9846 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