170 lines
6.2 KiB
Swift
170 lines
6.2 KiB
Swift
|
//
|
||
|
// QRCodeView.swift
|
||
|
// OTP
|
||
|
//
|
||
|
// Created by Shadowfacts on 8/23/21.
|
||
|
//
|
||
|
|
||
|
import SwiftUI
|
||
|
import OTPKit
|
||
|
import CoreImage.CIFilterBuiltins
|
||
|
|
||
|
struct QRCodeView: View {
|
||
|
let key: TOTPKey
|
||
|
@State private var image: UIImage?
|
||
|
@Environment(\.dismiss) private var dismiss
|
||
|
@State private var isPresentingShareSheet = false
|
||
|
|
||
|
init(key: TOTPKey) {
|
||
|
self.key = key
|
||
|
// self._image = State(initialValue: createImage())
|
||
|
}
|
||
|
|
||
|
private func createImage() -> UIImage? {
|
||
|
let filter = CIFilter.qrCodeGenerator()
|
||
|
filter.message = key.url.absoluteString.data(using: .utf8)!
|
||
|
let transform = CGAffineTransform(scaleX: 5, y: 5)
|
||
|
|
||
|
let context = CIContext()
|
||
|
guard let output = filter.outputImage?.transformed(by: transform),
|
||
|
let cgImage = context.createCGImage(output, from: output.extent) else {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
let issuerFont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .largeTitle).withSymbolicTraits(.traitBold)!, size: 0)
|
||
|
let labelFont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 0)
|
||
|
|
||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||
|
paragraphStyle.lineBreakMode = .byWordWrapping
|
||
|
|
||
|
let issuer = key.issuer as NSString
|
||
|
let size = CGSize(width: CGFloat(cgImage.width - 8), height: .greatestFiniteMagnitude)
|
||
|
|
||
|
var issuerRect = issuer.boundingRect(with: size, options: [.usesFontLeading, .usesLineFragmentOrigin], attributes: [.font: issuerFont, .paragraphStyle: paragraphStyle], context: nil)
|
||
|
issuerRect.origin.y += 4
|
||
|
issuerRect.origin.x += 4
|
||
|
|
||
|
var labelRect = CGRect.zero
|
||
|
if let label = key.label, !label.isEmpty {
|
||
|
labelRect = (label as NSString).boundingRect(with: size, options: [.usesFontLeading, .usesLineFragmentOrigin], attributes: [.font: labelFont, .paragraphStyle: paragraphStyle], context: nil)
|
||
|
labelRect.origin.x += 4
|
||
|
labelRect.origin.y += ceil(issuerRect.maxY)
|
||
|
}
|
||
|
|
||
|
let imageSize = CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height) + ceil(issuerRect.height) + ceil(labelRect.height) + 4)
|
||
|
let renderer = UIGraphicsImageRenderer(size: imageSize)
|
||
|
return renderer.image { (ctx) in
|
||
|
ctx.cgContext.setFillColor(UIColor.white.cgColor)
|
||
|
ctx.cgContext.fill(CGRect(origin: .zero, size: imageSize))
|
||
|
|
||
|
issuer.draw(in: issuerRect, withAttributes: [.font: issuerFont, .paragraphStyle: paragraphStyle])
|
||
|
|
||
|
if let label = key.label {
|
||
|
(label as NSString).draw(in: labelRect, withAttributes: [.font: labelFont, .paragraphStyle: paragraphStyle])
|
||
|
}
|
||
|
|
||
|
let imageRect = CGRect(x: 0, y: imageSize.height - CGFloat(cgImage.height), width: CGFloat(cgImage.width), height: CGFloat(cgImage.height))
|
||
|
ctx.cgContext.draw(cgImage, in: imageRect)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var body: some View {
|
||
|
return mainView
|
||
|
.navigationTitle("Export QR Code")
|
||
|
.toolbar {
|
||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||
|
Button("Done") {
|
||
|
dismiss()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
.background {
|
||
|
ActivityView(isPresented: $isPresentingShareSheet, items: [image as Any, key.url])
|
||
|
}
|
||
|
.task {
|
||
|
self.image = createImage()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@ViewBuilder
|
||
|
private var mainView: some View {
|
||
|
if let image = image {
|
||
|
VStack(alignment: .center) {
|
||
|
Image(uiImage: image)
|
||
|
.cornerRadius(5)
|
||
|
.shadow(radius: 3)
|
||
|
|
||
|
HStack {
|
||
|
Button {
|
||
|
isPresentingShareSheet = true
|
||
|
} label: {
|
||
|
Label("Share", systemImage: "square.and.arrow.up")
|
||
|
}
|
||
|
Spacer()
|
||
|
Button(action: save) {
|
||
|
Label("Save", systemImage: "square.and.arrow.down")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// fix the size so that the HStack doesn't grow beyond with width of the image
|
||
|
.fixedSize()
|
||
|
} else {
|
||
|
ProgressView()
|
||
|
.progressViewStyle(.circular)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func save() {
|
||
|
guard let image = image else { return }
|
||
|
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct ActivityView: UIViewControllerRepresentable {
|
||
|
var isPresented: Binding<Bool>
|
||
|
let items: [Any]
|
||
|
|
||
|
func makeUIViewController(context: Context) -> Wrapper {
|
||
|
return Wrapper(isPresented: isPresented, items: items)
|
||
|
}
|
||
|
|
||
|
func updateUIViewController(_ uiViewController: Wrapper, context: Context) {
|
||
|
uiViewController.update(isPresented: isPresented, items: items)
|
||
|
}
|
||
|
|
||
|
class Wrapper: UIViewController {
|
||
|
private(set) var isPresented: Binding<Bool>!
|
||
|
private(set) var items: [Any]!
|
||
|
|
||
|
init(isPresented: Binding<Bool>, items: [Any]) {
|
||
|
self.isPresented = isPresented
|
||
|
self.items = items
|
||
|
|
||
|
super.init(nibName: nil, bundle: nil)
|
||
|
}
|
||
|
|
||
|
required init?(coder: NSCoder) {
|
||
|
fatalError("init(coder:) has not been implemented")
|
||
|
}
|
||
|
|
||
|
func update(isPresented: Binding<Bool>, items: [Any]) {
|
||
|
self.isPresented = isPresented
|
||
|
self.items = items
|
||
|
|
||
|
if isPresented.wrappedValue, presentedViewController == nil {
|
||
|
let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||
|
vc.completionWithItemsHandler = { (_, _, _, _) in
|
||
|
isPresented.wrappedValue = false
|
||
|
}
|
||
|
present(vc, animated: true)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct QRCodeView_Previews: PreviewProvider {
|
||
|
static var previews: some View {
|
||
|
QRCodeView(key: TOTPKey(urlComponents: URLComponents(string: "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")!)!)
|
||
|
}
|
||
|
}
|