taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }