From dc7408e98cd65717b36e5ba7ef5134d5b63598b8 Mon Sep 17 00:00:00 2001 From: Vansh Gandhi Date: Thu, 2 Nov 2023 12:34:30 -0700 Subject: [PATCH] Set config via QR code (#93) * Rename to RootView to match Android * Add OnboardingScreen * iOS 14 * ignoresSafeArea * remove redundant comment * Add QR code scanning * Add viewfinder * Restore smile_config for sample app --- Example/SmileID.xcodeproj/project.pbxproj | 32 +- Example/SmileID/AppDelegate.swift | 3 +- Example/SmileID/CodeScanner/CodeScanner.swift | 128 ++++ .../CodeScanner/ScannerViewController.swift | 560 ++++++++++++++++++ Example/SmileID/EnterUserIDView.swift | 3 +- Example/SmileID/HomeView.swift | 3 +- .../viewfinder.imageset/Contents.json | 15 + .../viewfinder.imageset/viewfinder.pdf | Bin 0 -> 4163 bytes Example/SmileID/OnboardingScreen.swift | 122 ++++ Example/SmileID/ResourcesView.swift | 4 +- .../{MainView.swift => RootView.swift} | 19 +- Example/SmileID/SettingsView.swift | 35 +- Example/SmileID/SmileConfigEntryView.swift | 23 +- .../BiometricKYC/IdInfoInputScreen.swift | 1 - .../Classes/Views/RadioGroupSelector.swift | 1 - .../Views/SearchableDropdownSelector.swift | 2 - 16 files changed, 900 insertions(+), 51 deletions(-) create mode 100644 Example/SmileID/CodeScanner/CodeScanner.swift create mode 100644 Example/SmileID/CodeScanner/ScannerViewController.swift create mode 100644 Example/SmileID/Images.xcassets/viewfinder.imageset/Contents.json create mode 100644 Example/SmileID/Images.xcassets/viewfinder.imageset/viewfinder.pdf create mode 100644 Example/SmileID/OnboardingScreen.swift rename Example/SmileID/{MainView.swift => RootView.swift} (76%) diff --git a/Example/SmileID.xcodeproj/project.pbxproj b/Example/SmileID.xcodeproj/project.pbxproj index 6a17540c5..fe2f4d067 100644 --- a/Example/SmileID.xcodeproj/project.pbxproj +++ b/Example/SmileID.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 1ECAE3872A2F69CD00653FCA /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ECAE3862A2F69BC00653FCA /* ToastView.swift */; }; 1ED53F6A2A2F28590020BEFB /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F602A2F28590020BEFB /* HomeViewModel.swift */; }; 1ED53F6B2A2F28590020BEFB /* ProductCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F612A2F28590020BEFB /* ProductCell.swift */; }; - 1ED53F6C2A2F28590020BEFB /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F622A2F28590020BEFB /* MainView.swift */; }; + 1ED53F6C2A2F28590020BEFB /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F622A2F28590020BEFB /* RootView.swift */; }; 1ED53F6D2A2F28590020BEFB /* SmileTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F632A2F28590020BEFB /* SmileTextField.swift */; }; 1ED53F6E2A2F28590020BEFB /* ResourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F642A2F28590020BEFB /* ResourcesView.swift */; }; 1ED53F712A2F28590020BEFB /* EnterUserIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED53F672A2F28590020BEFB /* EnterUserIDView.swift */; }; @@ -26,8 +26,11 @@ 58C7118C2A69DE920062BBFB /* EnhancedKycTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7118B2A69DE920062BBFB /* EnhancedKycTest.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; + 6AC9802B9D1A630961B5454B /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98436935FFEA40E632182 /* CodeScanner.swift */; }; 6AC983F056A8F9088D6CF3F7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC982147640002B81F72DEC /* SettingsView.swift */; }; + 6AC984526F49F4E8F52C7494 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98BA00298258573CBCBD4 /* ScannerViewController.swift */; }; 6AC9870BB28E40FCACC75947 /* DocumentVerificationIdTypeSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC9868BCF06ECE5F65DF248 /* DocumentVerificationIdTypeSelector.swift */; }; + 6AC98856053013D0E8ABB188 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC982F34F80CAE1AA5569AB /* OnboardingScreen.swift */; }; 6AC98990097662789B0107EB /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98BC49871655D87C7DEE3 /* SettingsViewModel.swift */; }; 6AC98B6FFA753C5463F7216F /* SmileConfigEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC984E484EEF69069C705C7 /* SmileConfigEntryView.swift */; }; 6AC98C0E9305B4B3EB66ED35 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC980584C522B17A099E098 /* Util.swift */; }; @@ -66,7 +69,7 @@ 1ECAE3862A2F69BC00653FCA /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; 1ED53F602A2F28590020BEFB /* HomeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 1ED53F612A2F28590020BEFB /* ProductCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductCell.swift; sourceTree = ""; }; - 1ED53F622A2F28590020BEFB /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 1ED53F622A2F28590020BEFB /* RootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 1ED53F632A2F28590020BEFB /* SmileTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmileTextField.swift; sourceTree = ""; }; 1ED53F642A2F28590020BEFB /* ResourcesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourcesView.swift; sourceTree = ""; }; 1ED53F672A2F28590020BEFB /* EnterUserIDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterUserIDView.swift; sourceTree = ""; }; @@ -87,9 +90,12 @@ 67420F8D15457A4FC46AFB84 /* Pods-SmileID_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SmileID_Example.debug.xcconfig"; path = "Target Support Files/Pods-SmileID_Example/Pods-SmileID_Example.debug.xcconfig"; sourceTree = ""; }; 6AC980584C522B17A099E098 /* Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 6AC982147640002B81F72DEC /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 6AC982F34F80CAE1AA5569AB /* OnboardingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = ""; }; + 6AC98436935FFEA40E632182 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = ""; }; 6AC984E484EEF69069C705C7 /* SmileConfigEntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmileConfigEntryView.swift; sourceTree = ""; }; 6AC9868BCF06ECE5F65DF248 /* DocumentVerificationIdTypeSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentVerificationIdTypeSelector.swift; sourceTree = ""; }; 6AC9893915EBA33F6984A6D9 /* DocumentSelectorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentSelectorViewModel.swift; sourceTree = ""; }; + 6AC98BA00298258573CBCBD4 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = ""; }; 6AC98BC49871655D87C7DEE3 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 915748522AA09A1F004A9F23 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; 916E487A2A4057B400C589FE /* smile_config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = smile_config.json; sourceTree = ""; }; @@ -159,7 +165,7 @@ 1ED53F672A2F28590020BEFB /* EnterUserIDView.swift */, 1ED53F692A2F28590020BEFB /* HomeView.swift */, 1ED53F602A2F28590020BEFB /* HomeViewModel.swift */, - 1ED53F622A2F28590020BEFB /* MainView.swift */, + 1ED53F622A2F28590020BEFB /* RootView.swift */, 1ED53F612A2F28590020BEFB /* ProductCell.swift */, 1ED53F642A2F28590020BEFB /* ResourcesView.swift */, 1ED53F632A2F28590020BEFB /* SmileTextField.swift */, @@ -180,6 +186,8 @@ 6AC982147640002B81F72DEC /* SettingsView.swift */, 6AC98BC49871655D87C7DEE3 /* SettingsViewModel.swift */, 6AC984E484EEF69069C705C7 /* SmileConfigEntryView.swift */, + 6AC982F34F80CAE1AA5569AB /* OnboardingScreen.swift */, + 6AC98DDBE27D169AF8DB4B98 /* CodeScanner */, ); name = Example; path = SmileID; @@ -235,6 +243,15 @@ name = Frameworks; sourceTree = ""; }; + 6AC98DDBE27D169AF8DB4B98 /* CodeScanner */ = { + isa = PBXGroup; + children = ( + 6AC98436935FFEA40E632182 /* CodeScanner.swift */, + 6AC98BA00298258573CBCBD4 /* ScannerViewController.swift */, + ); + path = CodeScanner; + sourceTree = ""; + }; 828BF541E068101B2E6ED55F /* Pods */ = { isa = PBXGroup; children = ( @@ -505,7 +522,7 @@ files = ( 1ECAE3872A2F69CD00653FCA /* ToastView.swift in Sources */, 1ED53F722A2F28590020BEFB /* SmileEnvironmentToggleButton.swift in Sources */, - 1ED53F6C2A2F28590020BEFB /* MainView.swift in Sources */, + 1ED53F6C2A2F28590020BEFB /* RootView.swift in Sources */, 1ED53F6E2A2F28590020BEFB /* ResourcesView.swift in Sources */, 1ED53F6A2A2F28590020BEFB /* HomeViewModel.swift in Sources */, 1E60ED372A29C306002695FF /* HomeViewController.swift in Sources */, @@ -523,6 +540,9 @@ 6AC983F056A8F9088D6CF3F7 /* SettingsView.swift in Sources */, 6AC98990097662789B0107EB /* SettingsViewModel.swift in Sources */, 6AC98B6FFA753C5463F7216F /* SmileConfigEntryView.swift in Sources */, + 6AC98856053013D0E8ABB188 /* OnboardingScreen.swift in Sources */, + 6AC9802B9D1A630961B5454B /* CodeScanner.swift in Sources */, + 6AC984526F49F4E8F52C7494 /* ScannerViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -688,7 +708,7 @@ INFOPLIST_FILE = SmileID/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Smile ID"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.2; MODULE_NAME = ExampleApp; @@ -715,7 +735,7 @@ INFOPLIST_FILE = SmileID/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Smile ID"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.2; MODULE_NAME = ExampleApp; diff --git a/Example/SmileID/AppDelegate.swift b/Example/SmileID/AppDelegate.swift index 832413b81..6f28c8401 100644 --- a/Example/SmileID/AppDelegate.swift +++ b/Example/SmileID/AppDelegate.swift @@ -4,7 +4,6 @@ import SwiftUI import netfox @UIApplicationMain -@available(iOS 14.0, *) class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? @@ -21,7 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // NOTE TO PARTNERS: Normally, you would call SmileID.initialize() here - window?.rootViewController = UIHostingController(rootView: MainView()) + window?.rootViewController = UIHostingController(rootView: RootView()) window?.makeKeyAndVisible() return true } diff --git a/Example/SmileID/CodeScanner/CodeScanner.swift b/Example/SmileID/CodeScanner/CodeScanner.swift new file mode 100644 index 000000000..e49457d4c --- /dev/null +++ b/Example/SmileID/CodeScanner/CodeScanner.swift @@ -0,0 +1,128 @@ +// swiftlint:disable all +// MIT License: https://github.com/twostraws/CodeScanner/blob/main/LICENSE +// CodeScanner.swift +// https://github.com/twostraws/CodeScanner +// +// Created by Paul Hudson on 14/12/2021. +// Copyright © 2021 Paul Hudson. All rights reserved. +// + +import AVFoundation +import SwiftUI + +/// An enum describing the ways CodeScannerView can hit scanning problems. +public enum ScanError: Error { + /// The camera could not be accessed. + case badInput + + /// The camera was not capable of scanning the requested codes. + case badOutput + + /// Initialization failed. + case initError(_ error: Error) + + /// The camera permission is denied + case permissionDenied +} + +/// The result from a successful scan: the string that was scanned, and also the type of data that was found. +/// The type is useful for times when you've asked to scan several different code types at the same time, because +/// it will report the exact code type that was found. +@available(macCatalyst 14.0, *) +public struct ScanResult { + /// The contents of the code. + public let string: String + + /// The type of code that was matched. + public let type: AVMetadataObject.ObjectType + + /// The image of the code that was matched + public let image: UIImage? + + /// The corner coordinates of the scanned code. + public let corners: [CGPoint] +} + +/// The operating mode for CodeScannerView. +public enum ScanMode { + /// Scan exactly one code, then stop. + case once + + /// Scan each code no more than once. + case oncePerCode + + /// Keep scanning all codes until dismissed. + case continuous + + /// Scan only when capture button is tapped. + case manual +} + +/// A SwiftUI view that is able to scan barcodes, QR codes, and more, and send back what was found. +/// To use, set `codeTypes` to be an array of things to scan for, e.g. `[.qr]`, and set `completion` to +/// a closure that will be called when scanning has finished. This will be sent the string that was detected or a `ScanError`. +/// For testing inside the simulator, set the `simulatedData` property to some test data you want to send back. +@available(macCatalyst 14.0, *) +public struct CodeScannerView: UIViewControllerRepresentable { + + public let codeTypes: [AVMetadataObject.ObjectType] + public let scanMode: ScanMode + public let manualSelect: Bool + public let scanInterval: Double + public let showViewfinder: Bool + public var simulatedData = "" + public var shouldVibrateOnSuccess: Bool + public var isTorchOn: Bool + public var isGalleryPresented: Binding + public var videoCaptureDevice: AVCaptureDevice? + public var completion: (Result) -> Void + + public init( + codeTypes: [AVMetadataObject.ObjectType], + scanMode: ScanMode = .once, + manualSelect: Bool = false, + scanInterval: Double = 2.0, + showViewfinder: Bool = false, + simulatedData: String = "", + shouldVibrateOnSuccess: Bool = true, + isTorchOn: Bool = false, + isGalleryPresented: Binding = .constant(false), + videoCaptureDevice: AVCaptureDevice? = AVCaptureDevice.bestForVideo, + completion: @escaping (Result) -> Void + ) { + self.codeTypes = codeTypes + self.scanMode = scanMode + self.manualSelect = manualSelect + self.showViewfinder = showViewfinder + self.scanInterval = scanInterval + self.simulatedData = simulatedData + self.shouldVibrateOnSuccess = shouldVibrateOnSuccess + self.isTorchOn = isTorchOn + self.isGalleryPresented = isGalleryPresented + self.videoCaptureDevice = videoCaptureDevice + self.completion = completion + } + + public func makeUIViewController(context: Context) -> ScannerViewController { + return ScannerViewController(showViewfinder: showViewfinder, parentView: self) + } + + public func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) { + uiViewController.parentView = self + uiViewController.updateViewController( + isTorchOn: isTorchOn, + isGalleryPresented: isGalleryPresented.wrappedValue, + isManualCapture: scanMode == .manual, + isManualSelect: manualSelect + ) + } +} + +@available(macCatalyst 14.0, *) +struct CodeScannerView_Previews: PreviewProvider { + static var previews: some View { + CodeScannerView(codeTypes: [.qr]) { result in + // do nothing + } + } +} diff --git a/Example/SmileID/CodeScanner/ScannerViewController.swift b/Example/SmileID/CodeScanner/ScannerViewController.swift new file mode 100644 index 000000000..6bda346e0 --- /dev/null +++ b/Example/SmileID/CodeScanner/ScannerViewController.swift @@ -0,0 +1,560 @@ +// swiftlint:disable all +// MIT License: https://github.com/twostraws/CodeScanner/blob/main/LICENSE +// CodeScanner.swift +// https://github.com/twostraws/CodeScanner +// +// Created by Paul Hudson on 14/12/2021. +// Copyright © 2021 Paul Hudson. All rights reserved. +// + +import AVFoundation +import UIKit + +@available(macCatalyst 14.0, *) +extension CodeScannerView { + + public class ScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, AVCaptureMetadataOutputObjectsDelegate, UIAdaptivePresentationControllerDelegate { + private let photoOutput = AVCapturePhotoOutput() + private var isCapturing = false + private var handler: ((UIImage) -> Void)? + var parentView: CodeScannerView! + var codesFound = Set() + var didFinishScanning = false + var lastTime = Date(timeIntervalSince1970: 0) + private let showViewfinder: Bool + + let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video) + + private var isGalleryShowing: Bool = false { + didSet { + // Update binding + if parentView.isGalleryPresented.wrappedValue != isGalleryShowing { + parentView.isGalleryPresented.wrappedValue = isGalleryShowing + } + } + } + + public init(showViewfinder: Bool = false, parentView: CodeScannerView) { + self.parentView = parentView + self.showViewfinder = showViewfinder + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + self.showViewfinder = false + super.init(coder: coder) + } + + func openGallery() { + isGalleryShowing = true + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.presentationController?.delegate = self + present(imagePicker, animated: true, completion: nil) + } + + @objc func openGalleryFromButton(_ sender: UIButton) { + openGallery() + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + isGalleryShowing = false + + if let qrcodeImg = info[.originalImage] as? UIImage { + let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])! + let ciImage = CIImage(image: qrcodeImg)! + var qrCodeLink = "" + + let features = detector.features(in: ciImage) + + for feature in features as! [CIQRCodeFeature] { + qrCodeLink = feature.messageString! + if qrCodeLink == "" { + didFail(reason: .badOutput) + } else { + let corners = [ + feature.bottomLeft, + feature.bottomRight, + feature.topRight, + feature.topLeft + ] + let result = ScanResult(string: qrCodeLink, type: .qr, image: qrcodeImg, corners: corners) + found(result) + } + } + } else { + print("Something went wrong") + } + + dismiss(animated: true, completion: nil) + } + + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + isGalleryShowing = false + dismiss(animated: true, completion: nil) + } + + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + // Gallery is no longer being presented + isGalleryShowing = false + } + + #if targetEnvironment(simulator) + override public func loadView() { + view = UIView() + view.isUserInteractionEnabled = true + + 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." + label.textAlignment = .center + + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Select a custom image", for: .normal) + button.setTitleColor(UIColor.systemBlue, for: .normal) + button.setTitleColor(UIColor.gray, for: .highlighted) + button.addTarget(self, action: #selector(openGalleryFromButton), for: .touchUpInside) + + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 50 + stackView.addArrangedSubview(label) + stackView.addArrangedSubview(button) + + view.addSubview(stackView) + + NSLayoutConstraint.activate([ + button.heightAnchor.constraint(equalToConstant: 50), + stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent?) { + // Send back their simulated data, as if it was one of the types they were scanning for + found(ScanResult( + string: parentView.simulatedData, + type: parentView.codeTypes.first ?? .qr, image: nil, corners: [] + )) + } + + #else + + var captureSession: AVCaptureSession? + var previewLayer: AVCaptureVideoPreviewLayer! + + private lazy var viewFinder: UIImageView? = { + guard let image = UIImage(named: "viewfinder") else { + return nil + } + + let imageView = UIImageView(image: image) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var manualCaptureButton: UIButton = { + let button = UIButton(type: .system) + let image = UIImage(systemName: "camera.aperture") + button.setBackgroundImage(image, for: .normal) + button.addTarget(self, action: #selector(manualCapturePressed), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var manualSelectButton: UIButton = { + let button = UIButton(type: .system) + let image = UIImage(systemName: "photo.on.rectangle") + let background = UIImage(systemName: "capsule.fill")?.withTintColor(.systemBackground, renderingMode: .alwaysOriginal) + button.setImage(image, for: .normal) + button.setBackgroundImage(background, for: .normal) + button.addTarget(self, action: #selector(openGalleryFromButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + override public func viewDidLoad() { + super.viewDidLoad() + self.addOrientationDidChangeObserver() + self.setBackgroundColor() + self.handleCameraPermission() + } + + override public func viewWillLayoutSubviews() { + previewLayer?.frame = view.layer.bounds + } + + @objc func updateOrientation() { + guard let orientation = view.window?.windowScene?.interfaceOrientation else { return } + guard let connection = captureSession?.connections.last, connection.isVideoOrientationSupported else { return } + switch orientation { + case .portrait: + connection.videoOrientation = .portrait + case .landscapeLeft: + connection.videoOrientation = .landscapeLeft + case .landscapeRight: + connection.videoOrientation = .landscapeRight + case .portraitUpsideDown: + connection.videoOrientation = .portraitUpsideDown + default: + connection.videoOrientation = .portrait + } + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + updateOrientation() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + setupSession() + } + + private func setupSession() { + guard let captureSession = captureSession else { + return + } + + if previewLayer == nil { + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + } + + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + addviewfinder() + + reset() + + if (captureSession.isRunning == false) { + DispatchQueue.global(qos: .userInteractive).async { + self.captureSession?.startRunning() + } + } + } + + private func handleCameraPermission() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .restricted: + break + case .denied: + self.didFail(reason: .permissionDenied) + case .notDetermined: + self.requestCameraAccess { + self.setupCaptureDevice() + DispatchQueue.main.async { + self.setupSession() + } + } + case .authorized: + self.setupCaptureDevice() + self.setupSession() + + default: + break + } + } + + private func requestCameraAccess(completion: (() -> Void)?) { + AVCaptureDevice.requestAccess(for: .video) { [weak self] status in + guard status else { + self?.didFail(reason: .permissionDenied) + return + } + completion?() + } + } + + private func addOrientationDidChangeObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(updateOrientation), + name: Notification.Name("UIDeviceOrientationDidChangeNotification"), + object: nil + ) + } + + private func setBackgroundColor(_ color: UIColor = .black) { + view.backgroundColor = color + } + + private func setupCaptureDevice() { + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = parentView.videoCaptureDevice ?? fallbackVideoCaptureDevice else { + return + } + + let videoInput: AVCaptureDeviceInput + + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + didFail(reason: .initError(error)) + return + } + + if (captureSession!.canAddInput(videoInput)) { + captureSession!.addInput(videoInput) + } else { + didFail(reason: .badInput) + return + } + let metadataOutput = AVCaptureMetadataOutput() + + if (captureSession!.canAddOutput(metadataOutput)) { + captureSession!.addOutput(metadataOutput) + captureSession?.addOutput(photoOutput) + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = parentView.codeTypes + } else { + didFail(reason: .badOutput) + return + } + } + + private func addviewfinder() { + guard showViewfinder, let imageView = viewFinder else { return } + + view.addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.widthAnchor.constraint(equalToConstant: 200), + imageView.heightAnchor.constraint(equalToConstant: 200), + ]) + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if (captureSession?.isRunning == true) { + DispatchQueue.global(qos: .userInteractive).async { + self.captureSession?.stopRunning() + } + } + + NotificationCenter.default.removeObserver(self) + } + + override public var prefersStatusBarHidden: Bool { + true + } + + override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { + .all + } + + /** Touch the screen for autofocus */ + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard touches.first?.view == view, + let touchPoint = touches.first, + let device = parentView.videoCaptureDevice ?? fallbackVideoCaptureDevice, + device.isFocusPointOfInterestSupported + else { return } + + let videoView = view + let screenSize = videoView!.bounds.size + let xPoint = touchPoint.location(in: videoView).y / screenSize.height + let yPoint = 1.0 - touchPoint.location(in: videoView).x / screenSize.width + let focusPoint = CGPoint(x: xPoint, y: yPoint) + + do { + try device.lockForConfiguration() + } catch { + return + } + + // Focus to the correct point, make continiuous focus and exposure so the point stays sharp when moving the device closer + device.focusPointOfInterest = focusPoint + device.focusMode = .continuousAutoFocus + device.exposurePointOfInterest = focusPoint + device.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure + device.unlockForConfiguration() + } + + @objc func manualCapturePressed(_ sender: Any?) { + self.readyManualCapture() + } + + func showManualCaptureButton(_ isManualCapture: Bool) { + if manualCaptureButton.superview == nil { + view.addSubview(manualCaptureButton) + NSLayoutConstraint.activate([ + manualCaptureButton.heightAnchor.constraint(equalToConstant: 60), + manualCaptureButton.widthAnchor.constraint(equalTo: manualCaptureButton.heightAnchor), + view.centerXAnchor.constraint(equalTo: manualCaptureButton.centerXAnchor), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: manualCaptureButton.bottomAnchor, constant: 32) + ]) + } + + view.bringSubviewToFront(manualCaptureButton) + manualCaptureButton.isHidden = !isManualCapture + } + + func showManualSelectButton(_ isManualSelect: Bool) { + if manualSelectButton.superview == nil { + view.addSubview(manualSelectButton) + NSLayoutConstraint.activate([ + manualSelectButton.heightAnchor.constraint(equalToConstant: 50), + manualSelectButton.widthAnchor.constraint(equalToConstant: 60), + view.centerXAnchor.constraint(equalTo: manualSelectButton.centerXAnchor), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: manualSelectButton.bottomAnchor, constant: 32) + ]) + } + + view.bringSubviewToFront(manualSelectButton) + manualSelectButton.isHidden = !isManualSelect + } + #endif + + func updateViewController(isTorchOn: Bool, isGalleryPresented: Bool, isManualCapture: Bool, isManualSelect: Bool) { + guard let videoCaptureDevice = parentView.videoCaptureDevice ?? fallbackVideoCaptureDevice else { + return + } + + if videoCaptureDevice.hasTorch { + try? videoCaptureDevice.lockForConfiguration() + videoCaptureDevice.torchMode = isTorchOn ? .on : .off + videoCaptureDevice.unlockForConfiguration() + } + + if isGalleryPresented && !isGalleryShowing { + openGallery() + } + + #if !targetEnvironment(simulator) + showManualCaptureButton(isManualCapture) + showManualSelectButton(isManualSelect) + #endif + } + + public func reset() { + codesFound.removeAll() + didFinishScanning = false + lastTime = Date(timeIntervalSince1970: 0) + } + + public func readyManualCapture() { + guard parentView.scanMode == .manual else { return } + self.reset() + lastTime = Date() + } + + public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + if let metadataObject = metadataObjects.first { + guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + guard let stringValue = readableObject.stringValue else { return } + + guard didFinishScanning == false else { return } + + let photoSettings = AVCapturePhotoSettings() + guard !isCapturing else { return } + isCapturing = true + + handler = { [self] image in + let result = ScanResult(string: stringValue, type: readableObject.type, image: image, corners: readableObject.corners) + + switch parentView.scanMode { + case .once: + found(result) + // make sure we only trigger scan once per use + didFinishScanning = true + + case .manual: + if !didFinishScanning, isWithinManualCaptureInterval() { + found(result) + didFinishScanning = true + } + + case .oncePerCode: + if !codesFound.contains(stringValue) { + codesFound.insert(stringValue) + found(result) + } + + case .continuous: + if isPastScanInterval() { + found(result) + } + } + } + photoOutput.capturePhoto(with: photoSettings, delegate: self) + } + } + + func isPastScanInterval() -> Bool { + Date().timeIntervalSince(lastTime) >= parentView.scanInterval + } + + func isWithinManualCaptureInterval() -> Bool { + Date().timeIntervalSince(lastTime) <= 0.5 + } + + func found(_ result: ScanResult) { + lastTime = Date() + + if parentView.shouldVibrateOnSuccess { + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + } + + parentView.completion(.success(result)) + } + + func didFail(reason: ScanError) { + parentView.completion(.failure(reason)) + } + } +} + +@available(macCatalyst 14.0, *) +extension CodeScannerView.ScannerViewController: AVCapturePhotoCaptureDelegate { + + public func photoOutput( + _ output: AVCapturePhotoOutput, + didFinishProcessingPhoto photo: AVCapturePhoto, + error: Error? + ) { + isCapturing = false + guard let imageData = photo.fileDataRepresentation() else { + print("Error while generating image from photo capture data."); + return + } + guard let qrImage = UIImage(data: imageData) else { + print("Unable to generate UIImage from image data."); + return + } + handler?(qrImage) + } + + public func photoOutput( + _ output: AVCapturePhotoOutput, + willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings + ) { + AudioServicesDisposeSystemSoundID(1108) + } + + public func photoOutput( + _ output: AVCapturePhotoOutput, + didCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings + ) { + AudioServicesDisposeSystemSoundID(1108) + } +} + +@available(macCatalyst 14.0, *) +public extension AVCaptureDevice { + + /// This returns the Ultra Wide Camera on capable devices and the default Camera for Video otherwise. + static var bestForVideo: AVCaptureDevice? { + let deviceHasUltraWideCamera = !AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInUltraWideCamera], mediaType: .video, position: .back).devices.isEmpty + return deviceHasUltraWideCamera ? AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) : AVCaptureDevice.default(for: .video) + } +} diff --git a/Example/SmileID/EnterUserIDView.swift b/Example/SmileID/EnterUserIDView.swift index 5b66c8b91..876b274f0 100644 --- a/Example/SmileID/EnterUserIDView.swift +++ b/Example/SmileID/EnterUserIDView.swift @@ -29,11 +29,10 @@ struct EnterUserIDView: View { Spacer() } .padding(.top, 50) - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) } } -@available(iOS 14.0, *) private struct EnterUserIDView_Previews: PreviewProvider { static var previews: some View { EnterUserIDView(initialUserId: "initialValue") { _ in } diff --git a/Example/SmileID/HomeView.swift b/Example/SmileID/HomeView.swift index 82dcf6cb8..2ac4154e6 100644 --- a/Example/SmileID/HomeView.swift +++ b/Example/SmileID/HomeView.swift @@ -88,7 +88,7 @@ struct HomeView: View { .padding() .navigationBarTitle(Text("Smile ID"), displayMode: .inline) .navigationBarItems(trailing: SmileEnvironmentToggleButton()) - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) } } } @@ -231,7 +231,6 @@ private struct MyVerticalGrid: View { } } -@available(iOS 14.0, *) private struct HomeView_Previews: PreviewProvider { static var previews: some View { let _ = SmileID.initialize( diff --git a/Example/SmileID/Images.xcassets/viewfinder.imageset/Contents.json b/Example/SmileID/Images.xcassets/viewfinder.imageset/Contents.json new file mode 100644 index 000000000..f3b37b649 --- /dev/null +++ b/Example/SmileID/Images.xcassets/viewfinder.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "viewfinder.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Example/SmileID/Images.xcassets/viewfinder.imageset/viewfinder.pdf b/Example/SmileID/Images.xcassets/viewfinder.imageset/viewfinder.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d3278df406b35b59e411f760e19158ef2e201072 GIT binary patch literal 4163 zcmbtX-*4MC5Ps)h!IuK*gK3eJC=v()G)_HaYt}5uu!pV>iYzA@Y}vDwq1|78g%+Y}7X50x(GG&N9=6IfeeOUzfS=^B&FA5B zI(%m4VW*LK)7kaabyK%;db`_{_f;#Gb+g!2`_0R)tmI?$R5wB!xv0yw|J7mTy4VU@ zeE)UdR_p6#xe>I@9Tvd$ukvhOKdlRJ^%nfd>Brro+SSd|DSrR5-L9&2)wD7Z7Z-9- zEur?N*#26qD>;2Pxtz=*`0#aGfngHT2W!gBqS|kZvf35RQzg!m})+t=30}qK)($G;eJwgW!o!If; z^r~JiVRKVfd-(~PrqKBLR1k@KQ2s}~XrK3za_p{$=9{Lyyxe@D)hg+pAHDRtr?l** zXm>R&)ZbtDgHK0Q2Pa8kZ_>l&_ok*eOUm}a(0aFTi9O`h!J7}o32@hEc*kw$H2xh6 z3w6l~o3`sHo@+@_qXELUCPdtB|2nQ%aS_GspNp{%SH@}CMH7z($;vn7SF=?ptMVL=A<@8nT!aKq;rqzBD9B+4hwk$uq)hC&DIcHqfmlHsr~H!9C8ZO?G>T_Y5P;x*Cv zk&z<`{&cR%4(MjmX5bsTIUb|qQ{^aVnn(EcxR=O?6#Q5le9FbwB6SM8BgZ(L24!7l zKr2fJ4i46pg2B<56^EnenMaN%xrfP->Kra*Hq#^KN})A^aY#fayaJvf=7^o=O0@RK z4~h)XA+{rwWdg%l-Z0H9$Cw=qEd|U@sbWwkF|FaZR}MvpLzTg?wh=Jpe3qrTWMHhK zQZSe&0f&biu(`?uxCo=*y%uQ7fN1d~witAnNHoJoL^GUGH_5EZ4EE0@N5Mew9uy#@ zM>q&ebK?-97{W=aRmk!L&5qj<(T?aoKbY`J#9U%L}TQl#)#=*Zyx%I&J}e*B>9SE z#00x~G)=N;$TJq4S0hZJXYGuM<2ZHB~v&K)BWK@Re)A2Z9W}}@&JUR*0 z5srM&v6Lqrv5`2|%OXf^a%g-JW<8Q=WsQ^3%w#7qAx3?cWe@A@EYa9Gz)%<$&kcq{ zcRXbn&Q7FBI+VuKyCL7Y9%EBw zhed;bsTVZ0cW&Gr$M)%L^Fqsy!*F+lASd=fxUP78j8}zO(H5)CQ@^%b^{OFXcDQZf z?6=}DX3~G@GMvW}c>j=H^hnH`hX{0D)YF8jG+*4tr|hN zW}hy!sXsLWQ^cvM7DL{NE}Of!W4kNrRfQWP^5*&qK{(rQ*40~iRxgl?#NYI8vq2cT etAF?VzW%o&#t?&jTkP77L+ec{-n@DLasD40x{)XV literal 0 HcmV?d00001 diff --git a/Example/SmileID/OnboardingScreen.swift b/Example/SmileID/OnboardingScreen.swift new file mode 100644 index 000000000..aba12c271 --- /dev/null +++ b/Example/SmileID/OnboardingScreen.swift @@ -0,0 +1,122 @@ +import SmileID +import SwiftUI + +struct OnboardingScreen: View { + @State private var showManualEntrySheet = false + @State private var showQrCodeScanner = false + @State private var errorMessage: String? + + var body: some View { + VStack(alignment: .leading) { + Image("SmileLogo") + .resizable() + .scaledToFit() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, maxHeight: 64) + .padding(.vertical, 100) + + Text("Welcome to our sample App!") + .font(SmileID.theme.header1) + .foregroundColor(SmileID.theme.accent) + .padding(.vertical) + + Text("To begin testing, you need to add a configuration from the Smile Portal") + .font(SmileID.theme.header4) + .foregroundColor(SmileID.theme.onLight) + .padding(.vertical) + + Link( + "https://portal.usesmileid.com/sdk", + destination: URL(string: "https://portal.usesmileid.com/sdk")! + ) + .font(SmileID.theme.body) + .foregroundColor(SmileID.theme.accent) + .padding(.vertical) + + Spacer() + + Button( + action: { showQrCodeScanner = true }, + label: { + Spacer() + HStack { + Image(systemName: "qrcode") + .foregroundColor(SmileID.theme.onDark) + Text("Scan Configuration QR Code") + .font(SmileID.theme.button) + } + .padding() + .frame(maxWidth: .infinity) + } + ) + .foregroundColor(SmileID.theme.onDark) + .background(SmileID.theme.accent) + .cornerRadius(60) + .frame(maxWidth: .infinity) + .padding(.horizontal) + .padding(.vertical, 4) + + Button( + action: { showManualEntrySheet = true }, + label: { + Text("Enter Configuration Manually") + .padding() + .font(SmileID.theme.button) + .frame(maxWidth: .infinity) + } + ) + .foregroundColor(SmileID.theme.accent) + .background(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 60) + .stroke(SmileID.theme.accent, lineWidth: 4) + ) + .cornerRadius(60) + .frame(maxWidth: .infinity) + .padding(.horizontal) + } + .padding() + .background(SmileID.theme.backgroundLightest.ignoresSafeArea()) + .sheet(isPresented: $showManualEntrySheet) { + let content = SmileConfigEntryView(errorMessage: errorMessage) { smileConfig in + if updateSmileConfig(smileConfig) { + showManualEntrySheet = false + } else { + errorMessage = "Invalid Smile Config" + } + } + if #available(iOS 16.0, *) { + content + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } else { + content + } + } + .sheet(isPresented: $showQrCodeScanner) { + CodeScannerView( + codeTypes: [.qr], + scanInterval: 1, + showViewfinder: true + ) { response in + if case let .success(result) = response { + let configJson = result.string + if updateSmileConfig(configJson) { + showQrCodeScanner = false + } + } + } + } + } +} + +private func updateSmileConfig(_ configJson: String) -> Bool { + do { + let _ = try JSONDecoder().decode(Config.self, from: configJson.data(using: .utf8)!) + UserDefaults.standard.set(configJson, forKey: "smileConfig") + return true + } catch { + print("Error decoding new config: \(error)") + return false + } +} diff --git a/Example/SmileID/ResourcesView.swift b/Example/SmileID/ResourcesView.swift index 8edb8d1ae..63b012bfb 100644 --- a/Example/SmileID/ResourcesView.swift +++ b/Example/SmileID/ResourcesView.swift @@ -46,9 +46,9 @@ struct ResourcesView: View { } .padding() .navigationBarTitle("Resources", displayMode: .large) - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) } - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) if #available(iOS 16.0, *) { scrollView.toolbarBackground(SmileID.theme.backgroundLight, for: .navigationBar) diff --git a/Example/SmileID/MainView.swift b/Example/SmileID/RootView.swift similarity index 76% rename from Example/SmileID/MainView.swift rename to Example/SmileID/RootView.swift index d915324b4..0651fb6ff 100644 --- a/Example/SmileID/MainView.swift +++ b/Example/SmileID/RootView.swift @@ -1,8 +1,7 @@ import SwiftUI import SmileID -@available(iOS 14.0, *) -struct MainView: View { +struct RootView: View { // This is set by the SettingsView @AppStorage("smileConfig") private var configJson = ( UserDefaults.standard.string(forKey: "smileConfig") ?? "" @@ -15,8 +14,9 @@ struct MainView: View { } var body: some View { - let configUrl = Bundle.main.url(forResource: "smile_config", withExtension: "json")! - let builtInConfig = try? jsonDecoder.decode(Config.self, from: Data(contentsOf: configUrl)) + // It is possible the app was built without a smile_config, so it may be null + let builtInConfig = Bundle.main.url(forResource: "smile_config", withExtension: "json") + .flatMap { try? jsonDecoder.decode(Config.self, from: Data(contentsOf: $0)) } let configFromUserStorage = try? jsonDecoder.decode( Config.self, from: configJson.data(using: .utf8)! @@ -47,19 +47,18 @@ struct MainView: View { } } .accentColor(SmileID.theme.accent) - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) - .edgesIgnoringSafeArea(.all) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) + .ignoresSafeArea() .preferredColorScheme(.light) } else { - Text("Under Construction") + OnboardingScreen() } } } -@available(iOS 14.0, *) -private struct MainView_Previews: PreviewProvider { +private struct RootView_Previews: PreviewProvider { static var previews: some View { - MainView() + RootView() } } diff --git a/Example/SmileID/SettingsView.swift b/Example/SmileID/SettingsView.swift index b6c58dbab..69d5351ec 100644 --- a/Example/SmileID/SettingsView.swift +++ b/Example/SmileID/SettingsView.swift @@ -15,30 +15,25 @@ struct SettingsView: View { ) Spacer() } + .padding() + .navigationBarTitle("Settings", displayMode: .large) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) + .ignoresSafeArea() .sheet(isPresented: $viewModel.showSheet) { - // Use a ZStack here so that the backgroundColor fills up the entire modal, - // otherwise some jarring white sections get left at the top and bottom - // https://stackoverflow.com/a/73561306 - ZStack { - SmileID.theme.backgroundLightest.edgesIgnoringSafeArea(.all) - let content = SmileConfigEntryView( - errorMessage: viewModel.errorMessage, - onNewSmileConfig: viewModel.updateSmileConfig - ) - if #available(iOS 16.0, *) { - content - .presentationDetents([.medium]) - .presentationDragIndicator(.visible) - } else { - content - } + let content = SmileConfigEntryView( + errorMessage: viewModel.errorMessage, + onNewSmileConfig: viewModel.updateSmileConfig + ) + if #available(iOS 16.0, *) { + content + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } else { + content } } - .padding() - .navigationBarTitle("Settings", displayMode: .large) - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) } - .background(SmileID.theme.backgroundLight.edgesIgnoringSafeArea(.all)) + .background(SmileID.theme.backgroundLight.ignoresSafeArea()) if #available(iOS 16.0, *) { scrollView.toolbarBackground(SmileID.theme.backgroundLight, for: .navigationBar) diff --git a/Example/SmileID/SmileConfigEntryView.swift b/Example/SmileID/SmileConfigEntryView.swift index 7dbb605eb..2e122609c 100644 --- a/Example/SmileID/SmileConfigEntryView.swift +++ b/Example/SmileID/SmileConfigEntryView.swift @@ -2,6 +2,7 @@ import SmileID import SwiftUI struct SmileConfigEntryView: View { + @State private var showQrCodeScanner = false private let errorMessage: String? private let onNewSmileConfig: (_ newConfig: String) -> Void @@ -75,11 +76,14 @@ struct SmileConfigEntryView: View { .padding(.vertical, 2) Button( - action: { print("TODO") }, + action: { showQrCodeScanner = true }, label: { - Text("Scan QR Code from Portal") + HStack { + Image(systemName: "qrcode") + Text("Scan QR Code from Portal") + .font(SmileID.theme.button) + } .padding() - .font(SmileID.theme.button) .frame(maxWidth: .infinity) } ) @@ -94,6 +98,19 @@ struct SmileConfigEntryView: View { .padding(.horizontal) .padding(.vertical, 2) } + .background(SmileID.theme.backgroundLightest.ignoresSafeArea()) + .sheet(isPresented: $showQrCodeScanner) { + CodeScannerView( + codeTypes: [.qr], + scanInterval: 1, + showViewfinder: true + ) { response in + if case let .success(result) = response { + let configJson = result.string + onNewSmileConfig(configJson) + } + } + } } } diff --git a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift b/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift index 6ec6814ab..d92e5febc 100644 --- a/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift +++ b/Sources/SmileID/Classes/BiometricKYC/IdInfoInputScreen.swift @@ -112,7 +112,6 @@ struct IdInfoInputScreen: View { } } -@available(iOS 14.0, *) private struct IdInfoInputScreen_Previews: PreviewProvider { static var previews: some View { IdInfoInputScreen( diff --git a/Sources/SmileID/Classes/Views/RadioGroupSelector.swift b/Sources/SmileID/Classes/Views/RadioGroupSelector.swift index ace15646f..580671f2c 100644 --- a/Sources/SmileID/Classes/Views/RadioGroupSelector.swift +++ b/Sources/SmileID/Classes/Views/RadioGroupSelector.swift @@ -73,7 +73,6 @@ public struct RadioGroupSelector: View where T: Identifiable & Equatable { } } -@available(iOS 14.0, *) private struct RadioGroupSelector_Previews: PreviewProvider { static var previews: some View { let first = IdType(code: "id1", example: [], hasBack: true, name: "ID 1") diff --git a/Sources/SmileID/Classes/Views/SearchableDropdownSelector.swift b/Sources/SmileID/Classes/Views/SearchableDropdownSelector.swift index c05c5a0e8..3c9c50e11 100644 --- a/Sources/SmileID/Classes/Views/SearchableDropdownSelector.swift +++ b/Sources/SmileID/Classes/Views/SearchableDropdownSelector.swift @@ -74,7 +74,6 @@ public struct SearchableDropdownSelector: View { } } -@available(iOS 14.0, *) private struct SearchableDropdownSelectorUnselected_Previews: PreviewProvider { static var previews: some View { SearchableDropdownSelector( @@ -95,7 +94,6 @@ private struct SearchableDropdownSelectorUnselected_Previews: PreviewProvider { } } -@available(iOS 14.0, *) private struct SearchableDropdownSelectorSelected_Previews: PreviewProvider { static var previews: some View { let first = ValidDocument(