Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/Appearance/AppIconSettingsTableViewController.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

protocol AppIconSettingsTableViewControllerDelegate: AnyObject {
    func didChangeIcon()
}

final class AppIconSettingsTableViewController: OWSTableViewController2 {

    // MARK: Static properties

    private static let customIcons: [[AppIcon]] = [
        [.default, .white, .color, .night],
        [.nightVariant, .chat, .bubbles, .yellow],
        [.news, .notes, .weather, .waves],
    ]

    /// This URL itself is not used. The action is overridden in the text view delegate function.
    private static let learnMoreURL = URL(string: "https://support.signal.org/")!

    // MARK: Properties

    weak var iconDelegate: AppIconSettingsTableViewControllerDelegate?
    private var stackView: UIStackView?

    // MARK: View lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        title = OWSLocalizedString(
            "SETTINGS_APP_ICON_TITLE",
            comment: "The title for the app icon selection settings page.",
        )
        updateTableContents()
    }

    override func themeDidChange() {
        super.themeDidChange()
        updateTableContents()
    }

    // MARK: Table setup

    private func updateTableContents() {
        let contents = OWSTableContents()

        let section = OWSTableSection()
        section.add(.init(customCellBlock: { [weak self] in
            guard let self else { return UITableViewCell() }
            return self.buildIconSelectionCell()
        }))
        section.footerAttributedTitle = NSAttributedString.composed(of: [
            OWSLocalizedString(
                "SETTINGS_APP_ICON_FOOTER",
                comment: "The footer for the app icon selection settings page.",
            ),
            "\n",
            CommonStrings.learnMore.styled(with: .link(Self.learnMoreURL)),
        ])
        .styled(with: defaultFooterTextStyle)
        section.footerTextViewDelegate = self
        section.shouldDisableCellSelection = true

        contents.add(section)
        self.contents = contents
    }

    private func buildIconSelectionCell() -> UITableViewCell {
        let isiOS26 = if #available(iOS 26.0, *) { true } else { false }
        let iconSize: CGFloat = switch (
            UIDevice.current.isNarrowerThanIPhone6,
            UIDevice.current.isPlusSizePhone,
            isiOS26,
        ) {
        case (true, _, false): 56
        case (true, _, true): 61.5
        case (_, true, false): 64
        case (_, true, true): 68
        case (_, _, false): 60
        case (_, _, true): 64
        }

        let rows = Self.customIcons.map { row in
            let icons = row.map { icon in
                IconButton(icon: icon, iconSize: iconSize) { [weak self] in
                    self?.didTapIcon(icon)
                }
            }
            let stackView = UIStackView(arrangedSubviews: [SpacerView(preferredWidth: 0)] + icons + [SpacerView(preferredWidth: 0)])
            stackView.axis = .horizontal
            stackView.distribution = .equalSpacing
            stackView.alignment = .center
            return stackView
        }

        let stackView = UIStackView(arrangedSubviews: rows)
        stackView.axis = .vertical
        stackView.spacing = 32
        stackView.distribution = .fillEqually
        stackView.alignment = .fill

        self.stackView = stackView
        let cell = OWSTableItem.newCell()
        cell.contentView.addSubview(stackView)
        // Subtract off the cell inner margins in favor of
        // the stack views' spacer views with equal spacing.
        stackView.autoPinEdgesToSuperviewMargins(with: .init(hMargin: -Self.cellHInnerMargin, vMargin: 24))
        return cell
    }

    private func didTapLearnMore() {
        let learnMoreViewController = AppIconLearnMoreTableViewController()
        let navigationController = OWSNavigationController(rootViewController: learnMoreViewController)
        presentFormSheet(navigationController, animated: true)
    }

    private func didTapIcon(_ icon: AppIcon) {
        guard UIApplication.shared.currentAppIcon != icon else { return }

        UIApplication.shared.setAlternateIconName(icon.alternateIconName) { error in
            if let error {
                owsFailDebug("Failed to update app icon: \(error)")
            }
        }
        updateIconSelection()
        iconDelegate?.didChangeIcon()
    }

    private func updateIconSelection() {
        let animator = UIViewPropertyAnimator(duration: 0.15, springDamping: 1, springResponse: 0.15)
        animator.addAnimations {
            self.stackView?.arrangedSubviews
                .compactMap { $0 as? UIStackView }
                .flatMap(\.arrangedSubviews)
                .forEach { view in
                    guard let iconButton = view as? IconButton else { return }
                    iconButton.updateSelectedState()
                }
        }
        animator.startAnimation()
    }

    private class IconButton: UIView {
        private let icon: AppIcon?
        private let iconSize: CGFloat
        private let button: UIView
        private let selectedOutlineView: UIView

        init(icon: AppIcon, iconSize: CGFloat, action: @escaping () -> Void) {
            self.icon = icon
            self.iconSize = iconSize
            self.button = Self.makeButton(for: icon, iconSize: iconSize, action: action)
            self.selectedOutlineView = UIView.container()
            super.init(frame: .zero)

            self.addSubview(selectedOutlineView)
            selectedOutlineView.autoPinEdgesToSuperviewEdges()
            selectedOutlineView.layer.cornerRadius = iconSize * 0.24 * (4 / 3)
            selectedOutlineView.layer.cornerCurve = .continuous
            let borderColor: UIColor = Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_black
            selectedOutlineView.layer.borderColor = borderColor.cgColor

            selectedOutlineView.addSubview(button)
            button.autoPinEdgesToSuperviewEdges()

            updateSelectedState()
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        func updateSelectedState() {
            if UIApplication.shared.currentAppIcon == icon {
                button.transform = .scale(0.8)
                selectedOutlineView.layer.borderWidth = 3
            } else {
                button.transform = .identity
                selectedOutlineView.layer.borderWidth = 0
            }
        }

        private static func makeButton(for icon: AppIcon, iconSize: CGFloat, action: @escaping () -> Void) -> UIView {
            let image = UIImage(resource: icon.previewImageResource)
            let button = OWSButton(block: action)
            button.setImage(image, for: .normal)
            button.autoSetDimensions(to: .square(iconSize))
            return button
        }
    }
}

// MARK: UITextViewDelegate

extension AppIconSettingsTableViewController: UITextViewDelegate {
    func textView(_ textView: UITextView, shouldInteractWith url: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        if url == Self.learnMoreURL {
            didTapLearnMore()
        }
        return false
    }
}