// // 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 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! private(set) var items: [Any]! init(isPresented: Binding, 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, 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")!)!) } }