PosQRView.swift (5221B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Bohdan, Volodymyr Potuzhnyi 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 import SwiftUI 18 import CoreImage.CIFilterBuiltins 19 20 21 private let qrCornerRadius: CGFloat = 0.08 22 private let qrStripeWidth: CGFloat = 0.025 23 private let qrDataFraction: CGFloat = 0.88 24 private let qrLogoFraction: CGFloat = 0.30 * qrDataFraction 25 26 struct PosQRView: View { 27 let text: String? 28 let size: CGFloat 29 30 @State private var qrImage: UIImage? 31 @State private var showQR = false 32 33 var body: some View { 34 let cornerRadius = size * qrCornerRadius 35 let qrSize = size * qrDataFraction 36 let logoWidth = size * qrLogoFraction 37 38 ZStack { 39 RoundedRectangle(cornerRadius: cornerRadius) 40 .fill(.white) 41 .frame(width: size, height: size) 42 43 SpinningBorder(size: size) 44 45 if showQR, let qrImage { 46 Image(uiImage: qrImage) 47 .interpolation(.none) 48 .resizable() 49 .scaledToFit() 50 .frame(width: qrSize, height: qrSize) 51 .transition(.opacity) 52 53 Image("taler-logo-qr") 54 .resizable() 55 .scaledToFit() 56 .frame(width: logoWidth) 57 .transition(.opacity) 58 } 59 } 60 .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) 61 .animation(.easeIn(duration: 0.3), value: showQR) 62 .onAppear { 63 if let text { 64 generateQRAsync(from: text) 65 } 66 } 67 .onChange(of: text) { newText in 68 if let newText { 69 generateQRAsync(from: newText) 70 } else { 71 showQR = false 72 qrImage = nil 73 } 74 } 75 } 76 77 private func generateQRAsync(from string: String) { 78 let targetSize = size * qrDataFraction 79 Task.detached(priority: .userInitiated) { 80 let context = CIContext() 81 let filter = CIFilter.qrCodeGenerator() 82 filter.message = Data(string.utf8) 83 filter.correctionLevel = "H" 84 guard let ciImage = filter.outputImage else { return } 85 86 let modules = ciImage.extent.width 87 let scale = max(1, Int(ceil(Double(targetSize) / modules))) 88 let transformed = ciImage.transformed(by: CGAffineTransform(scaleX: CGFloat(scale), y: CGFloat(scale))) 89 90 guard let cgImage = context.createCGImage(transformed, from: transformed.extent) else { return } 91 let image = UIImage(cgImage: cgImage) 92 93 await MainActor.run { 94 qrImage = image 95 showQR = true 96 } 97 } 98 } 99 } 100 101 private struct SpinningBorder: View { 102 let size: CGFloat 103 @State private var rotation: Double = 0 104 105 private let stripeColor = Color(red: 0.188, green: 0.278, blue: 0.639) 106 107 private var stripeSoft: Color { 108 Color( 109 red: 1.0 * 0.45 + 0.188 * 0.55, 110 green: 1.0 * 0.45 + 0.278 * 0.55, 111 blue: 1.0 * 0.45 + 0.639 * 0.55 112 ) 113 } 114 115 var body: some View { 116 let cornerRadius = size * qrCornerRadius 117 let stripeWidth = size * qrStripeWidth 118 119 RoundedRectangle(cornerRadius: cornerRadius) 120 .strokeBorder( 121 AngularGradient( 122 gradient: Gradient(stops: [ 123 .init(color: .white, location: 0.00), 124 .init(color: .white, location: 0.05), 125 .init(color: stripeSoft, location: 0.09), 126 .init(color: stripeColor, location: 0.12), 127 .init(color: stripeSoft, location: 0.16), 128 .init(color: .white, location: 0.21), 129 .init(color: .white, location: 0.50), 130 .init(color: stripeSoft, location: 0.55), 131 .init(color: stripeColor, location: 0.59), 132 .init(color: stripeSoft, location: 0.62), 133 .init(color: .white, location: 0.66), 134 .init(color: .white, location: 1.00), 135 ]), 136 center: .center, 137 angle: .degrees(rotation) 138 ), 139 lineWidth: stripeWidth 140 ) 141 .frame(width: size, height: size) 142 .onAppear { 143 withAnimation(.linear(duration: 5.25).repeatForever(autoreverses: false)) { 144 rotation = 360 145 } 146 } 147 } 148 }