Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds QR Code Scanner UI component #14

Merged
merged 5 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/spruceid/wallet-sdk-rs.git",
"state" : {
"revision" : "56cd08960dd7ebabcaee435b5f15c8173e40fa1b",
"version" : "0.0.4"
"revision" : "d508f610c05a309a3254678d99c17804f04ec1fa",
"version" : "0.0.25"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "SpruceIDWalletSdk",
platforms: [
.iOS(.v13)
.iOS(.v14)
],
products: [
.library(
Expand Down
4 changes: 2 additions & 2 deletions Sources/WalletSdk/MDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CryptoKit
import Foundation
import SpruceIDWalletSdkRs

public typealias Namespace = String
public typealias MDocNamespace = String
public typealias IssuerSignedItemBytes = Data
public typealias ItemsRequest = SpruceIDWalletSdkRs.ItemsRequest

Expand All @@ -15,7 +15,7 @@ public class MDoc: Credential {
/// namespaces is the full set of namespaces with data items and their value
/// IssuerSignedItemBytes will be bytes, but its composition is defined here
/// https://github.com/spruceid/isomdl/blob/f7b05dfa/src/definitions/issuer_signed.rs#L18
public init?(fromMDoc issuerAuth: Data, namespaces: [Namespace: [IssuerSignedItemBytes]], keyAlias: String) {
public init?(fromMDoc issuerAuth: Data, namespaces: [MDocNamespace: [IssuerSignedItemBytes]], keyAlias: String) {
self.keyAlias = keyAlias
do {
try self.inner = SpruceIDWalletSdkRs.MDoc.fromCbor(value: issuerAuth)
Expand Down
288 changes: 288 additions & 0 deletions Sources/WalletSdk/ui/QRCodeScanner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import SwiftUI
import AVKit
import os.log

public class QRScannerDelegate: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {

@Published public var scannedCode: String?
public func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
if let metaObject = metadataObjects.first {
guard let readableObject = metaObject as? AVMetadataMachineReadableCodeObject else {return}
guard let scannedCode = readableObject.stringValue else {return}
self.scannedCode = scannedCode
}
}
}

/// Camera View Using AVCaptureVideoPreviewLayer
public struct CameraView: UIViewRepresentable {

var frameSize: CGSize

/// Camera Session
@Binding var session: AVCaptureSession

public init(frameSize: CGSize, session: Binding<AVCaptureSession>) {
self.frameSize = frameSize
self._session = session
}

public func makeUIView(context: Context) -> UIView {
/// Defining camera frame size
let view = UIViewType(frame: CGRect(origin: .zero, size: frameSize))
view.backgroundColor = .clear

let cameraLayer = AVCaptureVideoPreviewLayer(session: session)
cameraLayer.frame = .init(origin: .zero, size: frameSize)
cameraLayer.videoGravity = .resizeAspectFill
cameraLayer.masksToBounds = true
view.layer.addSublayer(cameraLayer)

return view
}

public func updateUIView(_ uiView: UIViewType, context: Context) {

}

}

extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}

public struct QRCodeScanner: View {
/// QR Code Scanner properties
@State private var isScanning: Bool = false
@State private var session: AVCaptureSession = .init()

/// QR scanner AV Output
@State private var qrOutput: AVCaptureMetadataOutput = .init()

/// Camera QR Output delegate
@StateObject private var qrDelegate = QRScannerDelegate()

/// Scanned code
@State private var scannedCode: String = ""

var title: String
var subtitle: String
var cancelButtonLabel: String
var onCancel: () -> Void
var onRead: (String) -> Void
var titleFont: Font?
var subtitleFont: Font?
var cancelButtonFont: Font?
var guidesColor: Color
var readerColor: Color
var textColor: Color
var backgroundOpacity: Double

public init(
title: String = "Scan QR Code",
subtitle: String = "Please align within the guides",
cancelButtonLabel: String = "Cancel",
onRead: @escaping (String) -> Void,
onCancel: @escaping () -> Void,
titleFont: Font? = nil,
subtitleFont: Font? = nil,
cancelButtonFont: Font? = nil,
guidesColor: Color = .white,
readerColor: Color = .white,
textColor: Color = .white,
backgroundOpacity: Double = 0.75
) {
self.title = title
self.subtitle = subtitle
self.cancelButtonLabel = cancelButtonLabel
self.onCancel = onCancel
self.onRead = onRead
self.titleFont = titleFont
self.subtitleFont = subtitleFont
self.cancelButtonFont = cancelButtonFont
self.guidesColor = guidesColor
self.readerColor = readerColor
self.textColor = textColor
self.backgroundOpacity = backgroundOpacity
}

public var body: some View {

ZStack {
GeometryReader {
let viewSize = $0.size
let size = UIScreen.screenSize
ZStack {
CameraView(frameSize: CGSize(width: size.width, height: size.height), session: $session)
/// Blur layer with clear cut out
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(backgroundOpacity))
.frame(width: size.width, height: UIScreen.screenHeight)
Rectangle()
.frame(width: size.width * 0.6, height: size.width * 0.6)
.blendMode(.destinationOut)
}
.compositingGroup()

/// Scan area edges
ZStack {
ForEach(0...4, id: \.self) { index in
let rotation = Double(index) * 90

RoundedRectangle(cornerRadius: 2, style: .circular)
/// Triming to get Scanner lik Edges
.trim(from: 0.61, to: 0.64)
.stroke(
guidesColor,
style: StrokeStyle(
lineWidth: 5,
lineCap: .round,
lineJoin: .round
)
)
.rotationEffect(.init(degrees: rotation))
}
/// Scanner Animation
Rectangle()
.fill(readerColor)
.frame(height: 2.5)
.offset(y: isScanning ? (size.width * 0.59)/2 : -(size.width * 0.59)/2)
}
.frame(width: size.width * 0.6, height: size.width * 0.6)

}
/// Square Shape
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

VStack(alignment: .leading) {
Text(title)
.font(titleFont)
.foregroundColor(textColor)

Text(subtitle)
.font(subtitleFont)
.foregroundColor(textColor)

Spacer()

Button(cancelButtonLabel) {
onCancel()
}
.font(cancelButtonFont)
.foregroundColor(textColor)
}
.padding(.vertical, 80)
}

/// Checking camera permission, when the view is visible
.onAppear(perform: {
Task {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
if session.inputs.isEmpty {
/// New setup
setupCamera()
} else {
/// Already existing one
reactivateCamera()
}
default: break
}
}
})

.onDisappear {
session.stopRunning()
}

.onChange(of: qrDelegate.scannedCode) { newValue in
if let code = newValue {
scannedCode = code

/// When the first code scan is available, immediately stop the camera.
session.stopRunning()

/// Stopping scanner animation
deActivateScannerAnimation()
/// Clearing the data on delegate
qrDelegate.scannedCode = nil

onRead(code)
}

}

}

func reactivateCamera() {
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
}

/// Activating Scanner Animation Method
func activateScannerAnimation() {
/// Adding Delay for each reversal
withAnimation(.easeInOut(duration: 0.85).delay(0.1).repeatForever(autoreverses: true)) {
isScanning = true
}
}

/// DeActivating scanner animation method
func deActivateScannerAnimation() {
/// Adding Delay for each reversal
withAnimation(.easeInOut(duration: 0.85)) {
isScanning = false
}
}

/// Setting up camera
func setupCamera() {
do {
/// Finding back camera
guard let device = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInUltraWideCamera, .builtInWideAngleCamera],
mediaType: .video, position: .back)
.devices.first
else {
os_log("Error: %@", log: .default, type: .error, String("UNKNOWN DEVICE ERROR"))
return
}

/// Camera input
let input = try AVCaptureDeviceInput(device: device)
/// For Extra Safety
/// Checking whether input & output can be added to the session
guard session.canAddInput(input), session.canAddOutput(qrOutput) else {
os_log("Error: %@", log: .default, type: .error, String("UNKNOWN INPUT/OUTPUT ERROR"))
return
}

/// Adding input & output to camera session
session.beginConfiguration()
session.addInput(input)
session.addOutput(qrOutput)
/// Setting output config to read qr codes
qrOutput.metadataObjectTypes = [.qr]
/// Adding delegate to retreive the fetched qr code from camera
qrOutput.setMetadataObjectsDelegate(qrDelegate, queue: .main)
session.commitConfiguration()
/// Note session must be started on background thread

DispatchQueue.global(qos: .background).async {
session.startRunning()
}
activateScannerAnimation()
} catch {
os_log("Error: %@", log: .default, type: .error, error.localizedDescription)
}
}
}
Loading