// // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalUI import SignalServiceKit import SwiftUI class QRCodeView: UIView { private let qrCodeTintColor: QRCodeColor private let loadingSpinner = UIActivityIndicatorView() private let qrCodeImageView = UIImageView() private let errorImageView: UIImageView = .withTemplateImageName("error-circle", tintColor: .ows_gray25) init( qrCodeTintColor: QRCodeColor = .blue, contentInset: CGFloat = 20, cornerRadius: CGFloat = 12, borderWidth: CGFloat = 2 ) { self.qrCodeTintColor = qrCodeTintColor super.init(frame: .zero) // MARK: View properties backgroundColor = .white layoutMargins = UIEdgeInsets(margin: contentInset) layer.cornerRadius = cornerRadius layer.borderWidth = borderWidth layer.borderColor = qrCodeTintColor.paddingBorder.cgColor loadingSpinner.style = .large loadingSpinner.color = Theme.lightThemePrimaryColor loadingSpinner.hidesWhenStopped = true // Don't antialias QR codes qrCodeImageView.layer.magnificationFilter = .nearest qrCodeImageView.layer.minificationFilter = .nearest // MARK: Layout addSubview(loadingSpinner) loadingSpinner.autoSetDimensions(to: .square(40)) loadingSpinner.autoPinEdgesToSuperviewMargins() addSubview(qrCodeImageView) qrCodeImageView.autoPinEdgesToSuperviewMargins() qrCodeImageView.contentMode = .scaleAspectFit addSubview(errorImageView) errorImageView.autoSetDimensions(to: .square(40)) errorImageView.autoCenterInSuperviewMargins() setLoading() } required init?(coder: NSCoder) { owsFail("Not implemented!") } // MARK: - private enum Mode { case loadingSpinner case qrCodeImage(UIImage) case errorImage } private func setMode(_ mode: Mode) { switch mode { case .loadingSpinner: loadingSpinner.startAnimating() qrCodeImageView.isHidden = true errorImageView.isHidden = true case .qrCodeImage(let image): loadingSpinner.stopAnimating() qrCodeImageView.isHidden = false errorImageView.isHidden = true qrCodeImageView.setTemplateImage(image, tintColor: qrCodeTintColor.foreground) case .errorImage: loadingSpinner.stopAnimating() qrCodeImageView.isHidden = true errorImageView.isHidden = false } } // MARK: - func setLoading() { setMode(.loadingSpinner) } func setError() { setMode(.errorImage) } func setQRCode(image: UIImage) { setMode(.qrCodeImage(image)) } func setQRCode( url: URL, stylingMode: QRCodeGenerator.StylingMode = .brandedWithLogo ) { let qrCodeImage = QRCodeGenerator().generateQRCode( url: url, stylingMode: stylingMode ) if let qrCodeImage { setMode(.qrCodeImage(qrCodeImage)) } else { setMode(.errorImage) } } } // MARK: - struct QRCodeViewRepresentable: UIViewRepresentable { class Model: ObservableObject { @Published var qrCodeURL: URL? init(qrCodeURL: URL?) { self.qrCodeURL = qrCodeURL } } @ObservedObject private var model: Model private let qrCodeStylingMode: QRCodeGenerator.StylingMode private let qrCodeTintColor: QRCodeColor private let contentInset: CGFloat private let cornerRadius: CGFloat private let borderWidth: CGFloat init( model: Model, qrCodeStylingMode: QRCodeGenerator.StylingMode = .brandedWithLogo, qrCodeTintColor: QRCodeColor = .blue, contentInset: CGFloat = 20, cornerRadius: CGFloat = 12, borderWidth: CGFloat = 2 ) { self.model = model self.qrCodeStylingMode = qrCodeStylingMode self.qrCodeTintColor = qrCodeTintColor self.contentInset = contentInset self.cornerRadius = cornerRadius self.borderWidth = borderWidth } // MARK: - typealias UIViewType = QRCodeView func makeUIView(context: Context) -> QRCodeView { let qrCodeView = QRCodeView( qrCodeTintColor: qrCodeTintColor, contentInset: contentInset, cornerRadius: cornerRadius, borderWidth: borderWidth ) updateUIView(qrCodeView, context: context) return qrCodeView } func updateUIView(_ qrCodeView: QRCodeView, context: Context) { if let url = model.qrCodeURL { qrCodeView.setQRCode( url: url, stylingMode: qrCodeStylingMode ) } else { qrCodeView.setLoading() } } } struct RotatingQRCodeView: View { class Model: ObservableObject { enum URLDisplayMode { case loading case loaded(URL) case refreshButton } @Published private(set) var urlDisplayMode: URLDisplayMode let onRefreshButtonPressed: () -> Void let qrCodeViewModel: QRCodeViewRepresentable.Model init(urlDisplayMode: URLDisplayMode, onRefreshButtonPressed: @escaping () -> Void) { self.urlDisplayMode = .loading self.onRefreshButtonPressed = onRefreshButtonPressed self.qrCodeViewModel = QRCodeViewRepresentable.Model(qrCodeURL: nil) updateURLDisplayMode(urlDisplayMode) } func updateURLDisplayMode(_ newValue: URLDisplayMode) { urlDisplayMode = newValue qrCodeViewModel.qrCodeURL = switch urlDisplayMode { case .loaded(let url): url case .loading, .refreshButton: nil } } } @ObservedObject var model: Model var body: some View { GeometryReader { qrCodeGeometry in ZStack { Color(UIColor.ows_gray02) .cornerRadius(24) switch model.urlDisplayMode { case .loading, .loaded: QRCodeViewRepresentable(model: model.qrCodeViewModel) .padding(qrCodeGeometry.size.height * 0.1) case .refreshButton: Button(action: model.onRefreshButtonPressed) { HStack { Image("refresh") Text(OWSLocalizedString( "SECONDARY_ONBOARDING_SCAN_CODE_REFRESH_CODE_BUTTON", comment: "Text for a button offering to refresh the QR code to link an iPad." )) .font(.body) .fontWeight(.bold) } .foregroundStyle(Color.black) .padding(.horizontal, 24) .padding(.vertical, 8) } .background { Capsule().fill(Color.white) } } } } .aspectRatio(1, contentMode: .fit) } } // MARK: - Previews #Preview { VStack { RotatingQRCodeView(model: .init( urlDisplayMode: .loaded(URL(string: "https://signal.org")!), onRefreshButtonPressed: {} )) RotatingQRCodeView(model: .init(urlDisplayMode: .loading, onRefreshButtonPressed: {})) RotatingQRCodeView(model: .init(urlDisplayMode: .refreshButton, onRefreshButtonPressed: {})) } .padding() }