Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/QRCodes/QRCodeView.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI
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()
}