OTP/OTP/Views/QRCodeView.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")!)!)
}
}