Initial commit
This commit is contained in:
366
Emoney Info/AboutViewController.swift
Normal file
366
Emoney Info/AboutViewController.swift
Normal file
@ -0,0 +1,366 @@
|
||||
// AboutViewController.swift
|
||||
// Emoney Info
|
||||
|
||||
import UIKit
|
||||
|
||||
class AboutViewController: UIViewController {
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.Color.background
|
||||
setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
(tabBarController as? MainTabBarController)?.setTabBarHidden(true, animated: animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
(tabBarController as? MainTabBarController)?.setTabBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
let scrollView = UIScrollView()
|
||||
let contentView = UIView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
||||
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
||||
])
|
||||
|
||||
// MARK: Back button + nav title
|
||||
let backButton = UIButton(type: .system)
|
||||
let chevron = UIImage(systemName: "chevron.left",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))
|
||||
backButton.setImage(chevron, for: .normal)
|
||||
backButton.tintColor = Theme.Color.textPrimary
|
||||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let navTitleLabel = UILabel()
|
||||
navTitleLabel.text = L10n.aboutAppTitle
|
||||
navTitleLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
navTitleLabel.textColor = Theme.Color.textSecondary
|
||||
navTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: App icon
|
||||
let iconView = UIImageView(image: UIImage(named: "AppLogo"))
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.layer.cornerRadius = 24
|
||||
iconView.clipsToBounds = true
|
||||
iconView.layer.shadowColor = UIColor.black.cgColor
|
||||
iconView.layer.shadowOpacity = 0.12
|
||||
iconView.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
iconView.layer.shadowRadius = 12
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: App name + version
|
||||
let appNameLabel = UILabel()
|
||||
appNameLabel.text = "Emoney Info"
|
||||
appNameLabel.font = .systemFont(ofSize: 28, weight: .bold)
|
||||
appNameLabel.textColor = Theme.Color.textPrimary
|
||||
appNameLabel.textAlignment = .center
|
||||
appNameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
let versionLabel = UILabel()
|
||||
let versionText = "VERSI \(appVersion)"
|
||||
let versionAttr = NSAttributedString(string: versionText, attributes: [
|
||||
.kern: 1.5,
|
||||
.font: Theme.Font.caption(weight: .semibold),
|
||||
.foregroundColor: Theme.Color.textSecondary,
|
||||
])
|
||||
versionLabel.attributedText = versionAttr
|
||||
versionLabel.textAlignment = .center
|
||||
versionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Description
|
||||
let descLabel = UILabel()
|
||||
descLabel.text = L10n.aboutAppDescription
|
||||
descLabel.font = Theme.Font.body(weight: .regular)
|
||||
descLabel.textColor = Theme.Color.textSecondary
|
||||
descLabel.numberOfLines = 0
|
||||
descLabel.textAlignment = .center
|
||||
descLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Feature chips
|
||||
let chipsStack = UIStackView(arrangedSubviews: [
|
||||
makeFeatureChip(L10n.aboutChipNfc),
|
||||
makeFeatureChip(L10n.aboutChipRealtime),
|
||||
makeFeatureChip(L10n.aboutChipMulti),
|
||||
])
|
||||
chipsStack.axis = .horizontal
|
||||
chipsStack.spacing = 10
|
||||
chipsStack.alignment = .center
|
||||
chipsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Legal rows card
|
||||
let legalCard = makeCard()
|
||||
|
||||
let termsRow = makeLegalRow(icon: "doc.text", title: L10n.aboutTerms)
|
||||
termsRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(termsTapped)))
|
||||
termsRow.isUserInteractionEnabled = true
|
||||
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = Theme.Color.background
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
|
||||
let privacyRow = makeLegalRow(icon: "lock.shield", title: L10n.aboutPrivacy)
|
||||
privacyRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(privacyTapped)))
|
||||
privacyRow.isUserInteractionEnabled = true
|
||||
|
||||
let legalStack = UIStackView(arrangedSubviews: [termsRow, separator, privacyRow])
|
||||
legalStack.axis = .vertical
|
||||
legalStack.spacing = 0
|
||||
legalStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
legalCard.addSubview(legalStack)
|
||||
|
||||
// MARK: Connect card (teal gradient)
|
||||
let connectCard = UIView()
|
||||
connectCard.layer.cornerRadius = 20
|
||||
connectCard.clipsToBounds = true
|
||||
connectCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let gradientLayer = CAGradientLayer()
|
||||
gradientLayer.colors = [
|
||||
Theme.Color.primary.cgColor,
|
||||
Theme.Color.secondary.cgColor,
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
|
||||
connectCard.layer.insertSublayer(gradientLayer, at: 0)
|
||||
|
||||
let connectIcon = UIImageView(image: UIImage(systemName: "wave.3.right.circle.fill",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .medium)))
|
||||
connectIcon.tintColor = UIColor.white.withAlphaComponent(0.35)
|
||||
connectIcon.contentMode = .scaleAspectFit
|
||||
connectIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let connectTitle = UILabel()
|
||||
connectTitle.text = L10n.aboutConnectTitle
|
||||
connectTitle.font = Theme.Font.subtitle(weight: .bold)
|
||||
connectTitle.textColor = .white
|
||||
connectTitle.numberOfLines = 0
|
||||
connectTitle.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let connectDesc = UILabel()
|
||||
connectDesc.text = L10n.aboutConnectDesc
|
||||
connectDesc.font = Theme.Font.caption(weight: .regular)
|
||||
connectDesc.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
connectDesc.numberOfLines = 0
|
||||
connectDesc.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[connectIcon, connectTitle, connectDesc].forEach { connectCard.addSubview($0) }
|
||||
|
||||
// MARK: Copyright
|
||||
let copyrightLabel = UILabel()
|
||||
copyrightLabel.text = L10n.footerCopyright
|
||||
copyrightLabel.font = Theme.Font.caption(weight: .regular)
|
||||
copyrightLabel.textColor = Theme.Color.textSecondary
|
||||
copyrightLabel.textAlignment = .center
|
||||
copyrightLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// MARK: Add to contentView
|
||||
[backButton, navTitleLabel, iconView, appNameLabel, versionLabel,
|
||||
descLabel, chipsStack, legalCard, connectCard, copyrightLabel]
|
||||
.forEach { contentView.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Nav row
|
||||
backButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 56),
|
||||
backButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
backButton.widthAnchor.constraint(equalToConstant: 32),
|
||||
backButton.heightAnchor.constraint(equalToConstant: 32),
|
||||
|
||||
navTitleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
||||
navTitleLabel.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 8),
|
||||
|
||||
// Icon
|
||||
iconView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
|
||||
iconView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 96),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 96),
|
||||
|
||||
// Name + version
|
||||
appNameLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 16),
|
||||
appNameLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: 6),
|
||||
versionLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
// Desc
|
||||
descLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 16),
|
||||
descLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32),
|
||||
descLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -32),
|
||||
|
||||
// Chips
|
||||
chipsStack.topAnchor.constraint(equalTo: descLabel.bottomAnchor, constant: 20),
|
||||
chipsStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
|
||||
// Legal card
|
||||
legalCard.topAnchor.constraint(equalTo: chipsStack.bottomAnchor, constant: 28),
|
||||
legalCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
legalCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
legalStack.topAnchor.constraint(equalTo: legalCard.topAnchor, constant: 4),
|
||||
legalStack.leadingAnchor.constraint(equalTo: legalCard.leadingAnchor),
|
||||
legalStack.trailingAnchor.constraint(equalTo: legalCard.trailingAnchor),
|
||||
legalStack.bottomAnchor.constraint(equalTo: legalCard.bottomAnchor, constant: -4),
|
||||
|
||||
// Connect card
|
||||
connectCard.topAnchor.constraint(equalTo: legalCard.bottomAnchor, constant: 20),
|
||||
connectCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
|
||||
connectCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
|
||||
|
||||
connectIcon.topAnchor.constraint(equalTo: connectCard.topAnchor, constant: 20),
|
||||
connectIcon.trailingAnchor.constraint(equalTo: connectCard.trailingAnchor, constant: -20),
|
||||
connectIcon.widthAnchor.constraint(equalToConstant: 44),
|
||||
connectIcon.heightAnchor.constraint(equalToConstant: 44),
|
||||
|
||||
connectTitle.topAnchor.constraint(equalTo: connectCard.topAnchor, constant: 24),
|
||||
connectTitle.leadingAnchor.constraint(equalTo: connectCard.leadingAnchor, constant: 20),
|
||||
connectTitle.trailingAnchor.constraint(equalTo: connectIcon.leadingAnchor, constant: -12),
|
||||
|
||||
connectDesc.topAnchor.constraint(equalTo: connectTitle.bottomAnchor, constant: 10),
|
||||
connectDesc.leadingAnchor.constraint(equalTo: connectCard.leadingAnchor, constant: 20),
|
||||
connectDesc.trailingAnchor.constraint(equalTo: connectCard.trailingAnchor, constant: -20),
|
||||
connectDesc.bottomAnchor.constraint(equalTo: connectCard.bottomAnchor, constant: -24),
|
||||
|
||||
// Copyright
|
||||
copyrightLabel.topAnchor.constraint(equalTo: connectCard.bottomAnchor, constant: 24),
|
||||
copyrightLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
copyrightLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -32),
|
||||
])
|
||||
|
||||
// Gradient frame — set after layout
|
||||
DispatchQueue.main.async {
|
||||
gradientLayer.frame = connectCard.bounds
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeCard() -> UIView {
|
||||
let v = UIView()
|
||||
v.backgroundColor = Theme.Color.card
|
||||
v.layer.cornerRadius = 16
|
||||
v.layer.shadowColor = UIColor.black.cgColor
|
||||
v.layer.shadowOpacity = 0.06
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
v.layer.shadowRadius = 8
|
||||
v.translatesAutoresizingMaskIntoConstraints = false
|
||||
return v
|
||||
}
|
||||
|
||||
private func makeFeatureChip(_ title: String) -> UIView {
|
||||
let container = UIView()
|
||||
container.backgroundColor = Theme.Color.primary.withAlphaComponent(0.12)
|
||||
container.layer.cornerRadius = 12
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = Theme.Font.caption(weight: .semibold)
|
||||
label.textColor = Theme.Color.secondary
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: container.topAnchor, constant: 6),
|
||||
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
|
||||
label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12),
|
||||
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -6),
|
||||
])
|
||||
return container
|
||||
}
|
||||
|
||||
private func makeLegalRow(icon: String, title: String) -> UIView {
|
||||
let row = UIView()
|
||||
row.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let iconContainer = UIView()
|
||||
iconContainer.backgroundColor = Theme.Color.background
|
||||
iconContainer.layer.cornerRadius = 10
|
||||
iconContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: icon,
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)))
|
||||
iconView.tintColor = Theme.Color.secondary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconContainer.addSubview(iconView)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.font = Theme.Font.body(weight: .medium)
|
||||
titleLabel.textColor = Theme.Color.textPrimary
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let chevronImg = UIImage(systemName: "chevron.right",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
|
||||
let chevron = UIImageView(image: chevronImg)
|
||||
chevron.tintColor = Theme.Color.textSecondary
|
||||
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[iconContainer, titleLabel, chevron].forEach { row.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
iconContainer.widthAnchor.constraint(equalToConstant: 36),
|
||||
iconContainer.heightAnchor.constraint(equalToConstant: 36),
|
||||
iconView.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor),
|
||||
iconView.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 18),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 18),
|
||||
|
||||
iconContainer.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 16),
|
||||
iconContainer.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
|
||||
titleLabel.leadingAnchor.constraint(equalTo: iconContainer.trailingAnchor, constant: 14),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8),
|
||||
|
||||
chevron.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -16),
|
||||
chevron.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
|
||||
row.heightAnchor.constraint(equalToConstant: 60),
|
||||
])
|
||||
return row
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func backTapped() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func termsTapped() {
|
||||
let vc = TermsViewController()
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
@objc private func privacyTapped() {
|
||||
let vc = PrivacyPolicyViewController()
|
||||
navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user