Files
simplex-chat/apps/ios/Shared/Views/NewChat/QRCode.swift
T
Evgeny @ SimpleX Chat 17c9ad1c61 ios, android, desktop: previews render the same in every snapshot run (#7938)
* ios, android, desktop: previews draw fixed sample data instead of calling the core

* desktop: database error preview passes the keychain setting

* ios: snapshot fixtures use fixed parsed markdown instead of the core

* ios: snapshot fixtures: raw string delimiters past the # in markdown JSON

* desktop: snapshot test sets the app bar height as the app does

* docs: plan to replace preview-only parameters with test measures

* ios, desktop: previews run master's code; badges and version views split

Preview-only parameters are removed. BadgesCheckOrderView, BadgesHowLongView and VersionView are split into a loader and a layout, which their previews draw. Snapshot tests set a fixed QR picture: the environment value fixedQRCode on iOS, LocalFixedQRCode on desktop.

* ios: snapshot fixtures build parsed markdown with initializers

The table held the JSON of the Linux core, which the iOS app does not decode. The values are those the core returns for each text.

* ci: references of fixed QR codes and version; ios: call overlay preview as on master

iOS references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40944118370, desktop references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40932566864. ActiveCallOverlay_Previews, its scene and its references are restored as on master.

* ios, desktop: snapshot tests render every preview, no exclusion lists

BadgesChooseBadgeView, ServersSummaryView and TerminalView are split into a loader and a layout. AddChannelView, AddGroupView, GroupProfileView and UserProfileView layouts take the name and description validators, so desktop previews render without the core library. NewChatView's preview opens the Connect tab, UserProfilesView's preview has its own users. AppearanceSettings, MigrateFromDevice and the duplicate UserProfileLayoutEditOn previews are deleted.

* ios, desktop: previews show short links; tests draw the fixed QR code with each app's generator

The snapshot tests draw the fixed QR picture for a short address link with generateImage on iOS and qrCodeBitmap on desktop, in place of committed images. Previews of group links, addresses, a pending connection and QR codes pass short links, as the apps show them. The fixture markdown table equals the output of master's parser.

* ios, desktop: excluded previews and their views as on master

* ci: references of short-link QR codes
2026-10-30 06:41:40 +00:00

149 lines
5.1 KiB
Swift

//
// QRCode.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import CoreImage.CIFilterBuiltins
import SimpleXChat
struct MutableQRCode: View {
@Binding var uri: String
var small: Bool = false
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var body: some View {
QRCode(uri: uri, small: small, withLogo: withLogo, tintColor: tintColor)
.id("simplex-qrcode-view-for-\(uri)")
}
}
struct SimpleXCreatedLinkQRCode: View {
let link: CreatedConnLink
@Binding var short: Bool
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: link.simplexChatUri(short: short), small: short && link.connShortLink != nil, onShare: onShare)
}
}
struct SimpleXLinkQRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: simplexChatLink(uri), small: uri.count < 200, withLogo: withLogo, tintColor: tintColor, onShare: onShare)
}
}
private let smallQRRatio: CGFloat = 0.63
extension EnvironmentValues {
struct FixedQRCode: EnvironmentKey {
static let defaultValue: UIImage? = nil
}
var fixedQRCode: UIImage? {
get { self[FixedQRCode.self] }
set { self[FixedQRCode.self] = newValue }
}
}
struct QRCode: View {
let uri: String
var small: Bool = false
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var onShare: (() -> Void)? = nil
@Environment(\.fixedQRCode) private var fixedQRCode
@State private var image: UIImage? = nil
@State private var makeScreenshotFunc: () -> Void = {}
@State private var width: CGFloat = .infinity
var body: some View {
ZStack {
if let image = fixedQRCode ?? image {
qrCodeImage(image).frame(width: width, height: width)
GeometryReader { g in
let w = g.size.width * (small ? smallQRRatio : 1)
let l = w * (small ? 0.195 : 0.16)
let m = w * 0.005
ZStack {
if withLogo {
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: l, height: l)
.frame(width: l + m, height: l + m)
.background(.white)
.clipShape(Circle())
}
}
.onAppear {
width = w
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(g.frame(in: .local).origin, size)])
onShare?()
}
}
.frame(width: g.size.width, height: g.size.height)
}
} else {
Color.clear.aspectRatio(small ? 1 / smallQRRatio : 1, contentMode: .fit)
}
}
.onTapGesture(perform: makeScreenshotFunc)
.task { if fixedQRCode == nil { image = await generateImage(uri, tintColor: tintColor, errorLevel: small ? "M" : "L") } }
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func qrCodeImage(_ image: UIImage) -> some View {
Image(uiImage: image)
.resizable()
.interpolation(.none)
.aspectRatio(1, contentMode: .fit)
.textSelection(.enabled)
}
func generateImage(_ uri: String, tintColor: UIColor, errorLevel: String) async -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
filter.correctionLevel = errorLevel
if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor)
}
return nil
}
extension View {
func makeScreenshot(_ origin: CGPoint? = nil, _ targetSize: CGSize? = nil) -> UIImage {
let controller = UIHostingController(rootView: self.edgesIgnoringSafeArea(.all))
let targetSize = targetSize ?? controller.view.intrinsicContentSize
let view = controller.view
view?.bounds = CGRect(origin: origin ?? .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
struct QRCode_Previews: PreviewProvider {
static var previews: some View {
QRCode(uri: "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw", small: true)
}
}