mirror of
https://codeberg.org/comaps/comaps
synced 2026-01-09 13:54:37 +00:00
Organic Maps sources as of 02.04.2025 (fad26bbf22ac3da75e01e62aa01e5c8e11861005)
To expand with full Organic Maps and Maps.ME commits history run: git remote add om-historic [om-historic.git repo url] git fetch --tags om-historic git replace squashed-history historic-commits
This commit is contained in:
453
iphone/Maps/UI/Help/AboutController/AboutController.swift
Normal file
453
iphone/Maps/UI/Help/AboutController/AboutController.swift
Normal file
@@ -0,0 +1,453 @@
|
||||
import OSLog
|
||||
|
||||
final class AboutController: MWMViewController {
|
||||
|
||||
fileprivate struct AboutInfoTableViewCellModel {
|
||||
let title: String
|
||||
let image: UIImage?
|
||||
let didTapHandler: (() -> Void)?
|
||||
}
|
||||
|
||||
fileprivate struct SocialMediaCollectionViewCellModel {
|
||||
let image: UIImage
|
||||
let didTapHandler: (() -> Void)?
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let infoTableViewCellHeight: CGFloat = 40
|
||||
static let socialMediaCollectionViewCellMaxWidth: CGFloat = 50
|
||||
static let socialMediaCollectionViewSpacing: CGFloat = 25
|
||||
static let socialMediaCollectionNumberOfItemsInRowCompact: CGFloat = 5
|
||||
static let socialMediaCollectionNumberOfItemsInRowRegular: CGFloat = 10
|
||||
}
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let stackView = UIStackView()
|
||||
private let logoImageView = UIImageView()
|
||||
private let headerTitleLabel = UILabel()
|
||||
private let additionalInfoStackView = UIStackView()
|
||||
private let donationView = DonationView()
|
||||
private let osmView = OSMView()
|
||||
private let infoTableView = UITableView(frame: .zero, style: .plain)
|
||||
private var infoTableViewHeightAnchor: NSLayoutConstraint?
|
||||
private let socialMediaHeaderLabel = UILabel()
|
||||
private let socialMediaCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
||||
private lazy var socialMediaCollectionViewHeighConstraint = socialMediaCollectionView.heightAnchor.constraint(equalToConstant: .zero)
|
||||
private let termsOfUseAndPrivacyPolicyView = ButtonsStackView()
|
||||
private var infoTableViewData = [AboutInfoTableViewCellModel]()
|
||||
private var socialMediaCollectionViewData = [SocialMediaCollectionViewCellModel]()
|
||||
private var onDidAppearCompletionHandler: (() -> Void)?
|
||||
|
||||
init(onDidAppearCompletionHandler: (() -> Void)? = nil) {
|
||||
self.onDidAppearCompletionHandler = onDidAppearCompletionHandler
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updateCollection()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if let completionHandler = onDidAppearCompletionHandler {
|
||||
completionHandler()
|
||||
onDidAppearCompletionHandler = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateCollection()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private extension AboutController {
|
||||
func setupViews() {
|
||||
func setupTitle() {
|
||||
let titleView = UILabel()
|
||||
titleView.text = Self.formattedAppVersion()
|
||||
titleView.textColor = .white
|
||||
titleView.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
titleView.isUserInteractionEnabled = true
|
||||
titleView.numberOfLines = 1
|
||||
titleView.allowsDefaultTighteningForTruncation = true
|
||||
titleView.adjustsFontSizeToFitWidth = true
|
||||
titleView.minimumScaleFactor = 0.5
|
||||
let titleDidTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(appVersionButtonTapped))
|
||||
titleView.addGestureRecognizer(titleDidTapGestureRecognizer)
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
||||
func setupScrollAndStack() {
|
||||
scrollView.delaysContentTouches = false
|
||||
scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
|
||||
|
||||
stackView.axis = .vertical
|
||||
stackView.distribution = .fill
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 15
|
||||
}
|
||||
|
||||
func setupLogo() {
|
||||
logoImageView.contentMode = .scaleAspectFit
|
||||
logoImageView.image = UIImage(named: "logo")
|
||||
}
|
||||
|
||||
func setupHeaderTitle() {
|
||||
headerTitleLabel.setFontStyle(.semibold18, color: .blackPrimary)
|
||||
headerTitleLabel.text = L("about_headline")
|
||||
headerTitleLabel.textAlignment = .center
|
||||
headerTitleLabel.numberOfLines = 1
|
||||
headerTitleLabel.allowsDefaultTighteningForTruncation = true
|
||||
headerTitleLabel.adjustsFontSizeToFitWidth = true
|
||||
headerTitleLabel.minimumScaleFactor = 0.5
|
||||
}
|
||||
|
||||
func setupAdditionalInfo() {
|
||||
additionalInfoStackView.axis = .vertical
|
||||
additionalInfoStackView.spacing = 15
|
||||
|
||||
[AboutInfo.noTracking, .noWifi, .community].forEach({ additionalInfoStackView.addArrangedSubview(InfoView(image: nil, title: $0.title)) })
|
||||
}
|
||||
|
||||
func setupDonation() {
|
||||
donationView.donateButtonDidTapHandler = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.openUrl(self.isDonateEnabled() ? Settings.donateUrl() : L("translated_om_site_url") + "support-us/")
|
||||
}
|
||||
}
|
||||
|
||||
func setupOSM() {
|
||||
osmView.setMapDate(Self.formattedMapsDataVersion())
|
||||
osmView.didTapHandler = { [weak self] in
|
||||
self?.openUrl("https://www.openstreetmap.org/")
|
||||
}
|
||||
}
|
||||
|
||||
func setupInfoTable() {
|
||||
infoTableView.setStyle(.clearBackground)
|
||||
infoTableView.delegate = self
|
||||
infoTableView.dataSource = self
|
||||
infoTableView.separatorStyle = .none
|
||||
infoTableView.isScrollEnabled = false
|
||||
infoTableView.showsVerticalScrollIndicator = false
|
||||
infoTableView.contentInset = .zero
|
||||
infoTableView.register(cell: InfoTableViewCell.self)
|
||||
}
|
||||
|
||||
func setupSocialMediaCollection() {
|
||||
socialMediaHeaderLabel.setFontStyle(.regular16, color: .blackPrimary)
|
||||
socialMediaHeaderLabel.text = L("follow_us")
|
||||
socialMediaHeaderLabel.numberOfLines = 1
|
||||
socialMediaHeaderLabel.allowsDefaultTighteningForTruncation = true
|
||||
socialMediaHeaderLabel.adjustsFontSizeToFitWidth = true
|
||||
socialMediaHeaderLabel.minimumScaleFactor = 0.5
|
||||
|
||||
socialMediaCollectionView.backgroundColor = .clear
|
||||
socialMediaCollectionView.isScrollEnabled = false
|
||||
socialMediaCollectionView.dataSource = self
|
||||
socialMediaCollectionView.delegate = self
|
||||
socialMediaCollectionView.register(cell: SocialMediaCollectionViewCell.self)
|
||||
}
|
||||
|
||||
func setupTermsAndPrivacy() {
|
||||
termsOfUseAndPrivacyPolicyView.addButton(title: L("privacy_policy"), didTapHandler: { [weak self] in
|
||||
self?.openUrl(L("translated_om_site_url") + "privacy/")
|
||||
})
|
||||
termsOfUseAndPrivacyPolicyView.addButton(title: L("terms_of_use"), didTapHandler: { [weak self] in
|
||||
self?.openUrl(L("translated_om_site_url") + "terms/")
|
||||
})
|
||||
termsOfUseAndPrivacyPolicyView.addButton(title: L("copyright"), didTapHandler: { [weak self] in
|
||||
self?.showCopyright()
|
||||
})
|
||||
}
|
||||
|
||||
view.setStyle(.pressBackground)
|
||||
|
||||
setupTitle()
|
||||
setupScrollAndStack()
|
||||
setupLogo()
|
||||
setupHeaderTitle()
|
||||
setupAdditionalInfo()
|
||||
setupDonation()
|
||||
setupOSM()
|
||||
setupInfoTable()
|
||||
setupSocialMediaCollection()
|
||||
setupTermsAndPrivacy()
|
||||
|
||||
infoTableViewData = buildInfoTableViewData()
|
||||
socialMediaCollectionViewData = buildSocialMediaCollectionViewData()
|
||||
}
|
||||
|
||||
func arrangeViews() {
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.addArrangedSubview(logoImageView)
|
||||
stackView.addArrangedSubview(headerTitleLabel)
|
||||
stackView.addArrangedSubviewWithSeparator(additionalInfoStackView)
|
||||
if isDonateEnabled() {
|
||||
stackView.addArrangedSubviewWithSeparator(donationView)
|
||||
}
|
||||
stackView.addArrangedSubviewWithSeparator(osmView)
|
||||
stackView.addArrangedSubviewWithSeparator(infoTableView)
|
||||
stackView.addArrangedSubviewWithSeparator(socialMediaHeaderLabel)
|
||||
stackView.addArrangedSubview(socialMediaCollectionView)
|
||||
stackView.addArrangedSubviewWithSeparator(termsOfUseAndPrivacyPolicyView)
|
||||
}
|
||||
|
||||
func layoutViews() {
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
logoImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
additionalInfoStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
donationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
infoTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
socialMediaCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
termsOfUseAndPrivacyPolicyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20),
|
||||
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20),
|
||||
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
||||
|
||||
logoImageView.heightAnchor.constraint(equalToConstant: 64),
|
||||
logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor),
|
||||
|
||||
additionalInfoStackView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
|
||||
osmView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
|
||||
infoTableView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
infoTableView.heightAnchor.constraint(equalToConstant: Constants.infoTableViewCellHeight * CGFloat(infoTableViewData.count)),
|
||||
|
||||
socialMediaHeaderLabel.leadingAnchor.constraint(equalTo: socialMediaCollectionView.leadingAnchor),
|
||||
|
||||
socialMediaCollectionView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
socialMediaCollectionView.contentLayoutGuide.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
socialMediaCollectionViewHeighConstraint,
|
||||
|
||||
termsOfUseAndPrivacyPolicyView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
|
||||
])
|
||||
donationView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = isDonateEnabled()
|
||||
|
||||
view.layoutIfNeeded()
|
||||
updateCollection()
|
||||
}
|
||||
|
||||
func updateCollection() {
|
||||
socialMediaCollectionView.collectionViewLayout.invalidateLayout()
|
||||
// On devices with the iOS 12 the actual collectionView layout update not always occurs during the current layout update cycle.
|
||||
// So constraints update should be performed on the next layout update cycle.
|
||||
DispatchQueue.main.async {
|
||||
self.socialMediaCollectionViewHeighConstraint.constant = self.socialMediaCollectionView.collectionViewLayout.collectionViewContentSize.height
|
||||
}
|
||||
}
|
||||
|
||||
func isDonateEnabled() -> Bool {
|
||||
return Settings.donateUrl() != nil
|
||||
}
|
||||
|
||||
func buildInfoTableViewData() -> [AboutInfoTableViewCellModel] {
|
||||
let infoContent: [AboutInfo] = [.faq, .reportMapDataProblem, .reportABug, .news, .volunteer, .rateTheApp]
|
||||
let data = infoContent.map { [weak self] aboutInfo in
|
||||
return AboutInfoTableViewCellModel(title: aboutInfo.title, image: aboutInfo.image, didTapHandler: {
|
||||
switch aboutInfo {
|
||||
case .faq:
|
||||
self?.navigationController?.pushViewController(FaqController(), animated: true)
|
||||
case .reportABug:
|
||||
MailComposer.sendBugReportWith(title:"Organic Maps Bug Report")
|
||||
case .reportMapDataProblem, .volunteer, .news:
|
||||
self?.openUrl(aboutInfo.link)
|
||||
case .rateTheApp:
|
||||
UIApplication.shared.rateApp()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func buildSocialMediaCollectionViewData() -> [SocialMediaCollectionViewCellModel] {
|
||||
let socialMediaContent: [SocialMedia] = [.telegram, .github, .instagram, .twitter, .linkedin, .organicMapsEmail, .reddit, .matrix, .facebook, .fosstodon]
|
||||
let data = socialMediaContent.map { [weak self] socialMedia in
|
||||
return SocialMediaCollectionViewCellModel(image: socialMedia.image, didTapHandler: {
|
||||
switch socialMedia {
|
||||
case .telegram: fallthrough
|
||||
case .github: fallthrough
|
||||
case .reddit: fallthrough
|
||||
case .matrix: fallthrough
|
||||
case .fosstodon: fallthrough
|
||||
case .facebook: fallthrough
|
||||
case .twitter: fallthrough
|
||||
case .instagram: fallthrough
|
||||
case .linkedin:
|
||||
self?.openUrl(socialMedia.link, externally: true)
|
||||
case .organicMapsEmail:
|
||||
MailComposer.sendEmail(toRecipients: [socialMedia.link])
|
||||
}
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// Returns a human-readable maps data version.
|
||||
static func formattedMapsDataVersion() -> String {
|
||||
// First, convert version code like 220131 to a date.
|
||||
let df = DateFormatter()
|
||||
df.locale = Locale(identifier:"en_US_POSIX")
|
||||
df.dateFormat = "yyMMdd"
|
||||
let mapsVersionInt = FrameworkHelper.dataVersion()
|
||||
let mapsDate = df.date(from: String(mapsVersionInt))!
|
||||
// Second, print the date in the local user's format.
|
||||
df.locale = Locale.current
|
||||
df.dateStyle = .long
|
||||
df.timeStyle = .none
|
||||
return df.string(from:mapsDate)
|
||||
}
|
||||
|
||||
static func formattedAppVersion() -> String {
|
||||
let appInfo = AppInfo.shared();
|
||||
// Use strong left-to-right unicode direction characters for the app version.
|
||||
return String(format: L("version"), "\u{2066}\(appInfo.bundleVersion)-\(appInfo.buildNumber)\u{2069}")
|
||||
}
|
||||
|
||||
func showCopyright() {
|
||||
let path = Bundle.main.path(forResource: "copyright", ofType: "html")!
|
||||
let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
|
||||
let webViewController = WebViewController.init(html: html, baseUrl: nil, title: L("copyright"))!
|
||||
webViewController.openInSafari = true
|
||||
self.navigationController?.pushViewController(webViewController, animated: true)
|
||||
}
|
||||
|
||||
func copyToClipboard(_ content: String) {
|
||||
UIPasteboard.general.string = content
|
||||
let message = String(format: L("copied_to_clipboard"), content)
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
Toast.toast(withText: message).show(withAlignment: .bottom, pinToSafeArea: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
private extension AboutController {
|
||||
@objc func appVersionButtonTapped() {
|
||||
copyToClipboard(Self.formattedAppVersion())
|
||||
}
|
||||
|
||||
@objc func osmMapsDataButtonTapped() {
|
||||
copyToClipboard(Self.formattedMapsDataVersion())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension AboutController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
infoTableViewData[indexPath.row].didTapHandler?()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return Constants.infoTableViewCellHeight
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension AboutController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return infoTableViewData.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(cell: InfoTableViewCell.self, indexPath: indexPath)
|
||||
let aboutInfo = infoTableViewData[indexPath.row]
|
||||
cell.set(image: aboutInfo.image, title: aboutInfo.title)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
extension AboutController: UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return socialMediaCollectionViewData.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(cell: SocialMediaCollectionViewCell.self, indexPath: indexPath)
|
||||
cell.setImage(socialMediaCollectionViewData[indexPath.row].image)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
extension AboutController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let model = socialMediaCollectionViewData[indexPath.row]
|
||||
model.didTapHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegateFlowLayout
|
||||
extension AboutController: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let spacing = Constants.socialMediaCollectionViewSpacing
|
||||
let numberOfItemsInRowCompact = Constants.socialMediaCollectionNumberOfItemsInRowCompact
|
||||
let numberOfItemsInRowRegular = Constants.socialMediaCollectionNumberOfItemsInRowRegular
|
||||
var totalSpacing = (Constants.socialMediaCollectionNumberOfItemsInRowCompact - 1) * spacing
|
||||
var width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowCompact
|
||||
if traitCollection.verticalSizeClass == .compact || traitCollection.horizontalSizeClass == .regular {
|
||||
totalSpacing = (numberOfItemsInRowRegular - 1) * spacing
|
||||
width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowRegular
|
||||
}
|
||||
let maxWidth = Constants.socialMediaCollectionViewCellMaxWidth
|
||||
width = min(width, maxWidth)
|
||||
return CGSize(width: width, height: width)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return Constants.socialMediaCollectionViewSpacing
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return Constants.socialMediaCollectionViewSpacing
|
||||
}
|
||||
}
|
||||
// MARK: - UIStackView + AddArrangedSubviewWithSeparator
|
||||
private extension UIStackView {
|
||||
func addArrangedSubviewWithSeparator(_ view: UIView) {
|
||||
if !arrangedSubviews.isEmpty {
|
||||
let separator = UIView()
|
||||
separator.setStyleAndApply(.divider)
|
||||
separator.isUserInteractionEnabled = false
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
addArrangedSubview(separator)
|
||||
NSLayoutConstraint.activate([
|
||||
separator.heightAnchor.constraint(equalToConstant: 1.0),
|
||||
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
}
|
||||
addArrangedSubview(view)
|
||||
}
|
||||
}
|
||||
71
iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift
Normal file
71
iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
enum AboutInfo {
|
||||
case faq
|
||||
case reportABug
|
||||
case reportMapDataProblem
|
||||
case volunteer
|
||||
case news
|
||||
case rateTheApp
|
||||
case noTracking
|
||||
case noWifi
|
||||
case community
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
|
||||
case .faq:
|
||||
return L("faq")
|
||||
case .reportABug:
|
||||
return L("report_a_bug")
|
||||
case .reportMapDataProblem:
|
||||
return L("report_incorrect_map_bug")
|
||||
case .volunteer:
|
||||
return L("volunteer")
|
||||
case .news:
|
||||
return L("news")
|
||||
case .rateTheApp:
|
||||
return L("rate_the_app")
|
||||
case .noTracking:
|
||||
return L("about_proposition_1")
|
||||
case .noWifi:
|
||||
return L("about_proposition_2")
|
||||
case .community:
|
||||
return L("about_proposition_3")
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage? {
|
||||
switch self {
|
||||
case .faq:
|
||||
return UIImage(named: "ic_about_faq")!
|
||||
case .reportABug:
|
||||
return UIImage(named: "ic_about_report_bug")!
|
||||
case .reportMapDataProblem:
|
||||
return UIImage(named: "ic_about_report_osm")!
|
||||
case .volunteer:
|
||||
return UIImage(named: "ic_about_volunteer")!
|
||||
case .news:
|
||||
return UIImage(named: "ic_about_news")!
|
||||
case .rateTheApp:
|
||||
return UIImage(named: "ic_about_rate_app")!
|
||||
case .noTracking, .noWifi, .community:
|
||||
// Dots are used for these cases
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var link: String? {
|
||||
switch self {
|
||||
case .faq, .rateTheApp, .noTracking, .noWifi, .community:
|
||||
// These cases don't provide redirection to the web
|
||||
return nil
|
||||
case .reportABug:
|
||||
return "ios@organicmaps.app"
|
||||
case .reportMapDataProblem:
|
||||
return "https://www.openstreetmap.org/fixthemap"
|
||||
case .volunteer:
|
||||
return L("translated_om_site_url") + "support-us/"
|
||||
case .news:
|
||||
return L("translated_om_site_url") + "news/"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift
Normal file
62
iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
enum SocialMedia {
|
||||
case telegram
|
||||
case twitter
|
||||
case instagram
|
||||
case facebook
|
||||
case reddit
|
||||
case matrix
|
||||
case fosstodon
|
||||
case linkedin
|
||||
case organicMapsEmail
|
||||
case github
|
||||
|
||||
var link: String {
|
||||
switch self {
|
||||
case .telegram:
|
||||
return L("telegram_url")
|
||||
case .github:
|
||||
return "https://github.com/organicmaps/organicmaps/"
|
||||
case .linkedin:
|
||||
return "https://www.linkedin.com/company/organic-maps/"
|
||||
case .organicMapsEmail:
|
||||
return "ios@organicmaps.app"
|
||||
case .matrix:
|
||||
return "https://matrix.to/#/#organicmaps:matrix.org"
|
||||
case .fosstodon:
|
||||
return "https://fosstodon.org/@organicmaps"
|
||||
case .facebook:
|
||||
return "https://facebook.com/OrganicMaps"
|
||||
case .twitter:
|
||||
return "https://twitter.com/OrganicMapsApp"
|
||||
case .instagram:
|
||||
return L("instagram_url")
|
||||
case .reddit:
|
||||
return "https://www.reddit.com/r/organicmaps/"
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .telegram:
|
||||
return UIImage(named: "ic_social_media_telegram")!
|
||||
case .github:
|
||||
return UIImage(named: "ic_social_media_github")!
|
||||
case .linkedin:
|
||||
return UIImage(named: "ic_social_media_linkedin")!
|
||||
case .organicMapsEmail:
|
||||
return UIImage(named: "ic_social_media_mail")!
|
||||
case .matrix:
|
||||
return UIImage(named: "ic_social_media_matrix")!
|
||||
case .fosstodon:
|
||||
return UIImage(named: "ic_social_media_fosstodon")!
|
||||
case .facebook:
|
||||
return UIImage(named: "ic_social_media_facebook")!
|
||||
case .twitter:
|
||||
return UIImage(named: "ic_social_media_x")!
|
||||
case .instagram:
|
||||
return UIImage(named: "ic_social_media_instagram")!
|
||||
case .reddit:
|
||||
return UIImage(named: "ic_social_media_reddit")!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
final class ButtonsStackView: UIView {
|
||||
|
||||
private let stackView = UIStackView()
|
||||
private var didTapHandlers = [UIButton: (() -> Void)?]()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
private func setupViews() {
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 20
|
||||
}
|
||||
|
||||
private func arrangeViews() {
|
||||
addSubview(stackView)
|
||||
}
|
||||
|
||||
private func layoutViews() {
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
let offset = CGFloat(20)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: offset),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -offset),
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func buttonTapped(_ sender: UIButton) {
|
||||
guard let didTapHandler = didTapHandlers[sender] else { return }
|
||||
didTapHandler?()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func addButton(title: String, font: UIFont = .regular14(), didTapHandler: @escaping () -> Void) {
|
||||
let button = UIButton()
|
||||
button.setStyleAndApply(.flatPrimaryTransButton)
|
||||
button.setTitle(title, for: .normal)
|
||||
button.titleLabel?.font = font
|
||||
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
|
||||
stackView.addArrangedSubview(button)
|
||||
didTapHandlers[button] = didTapHandler
|
||||
}
|
||||
}
|
||||
67
iphone/Maps/UI/Help/AboutController/Views/DonationView.swift
Normal file
67
iphone/Maps/UI/Help/AboutController/Views/DonationView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
final class DonationView: UIView {
|
||||
|
||||
private let donateTextLabel = UILabel()
|
||||
private let donateButton = UIButton()
|
||||
|
||||
var donateButtonDidTapHandler: (() -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
private func setupViews() {
|
||||
donateTextLabel.setFontStyle(.regular14, color: .blackPrimary)
|
||||
donateTextLabel.text = L("donate_description")
|
||||
donateTextLabel.textAlignment = .center
|
||||
donateTextLabel.lineBreakMode = .byWordWrapping
|
||||
donateTextLabel.numberOfLines = 0
|
||||
|
||||
donateButton.setStyle(.flatNormalButton)
|
||||
donateButton.setTitle(L("donate").localizedUppercase, for: .normal)
|
||||
donateButton.addTarget(self, action: #selector(donateButtonDidTap), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func arrangeViews() {
|
||||
addSubview(donateTextLabel)
|
||||
addSubview(donateButton)
|
||||
}
|
||||
|
||||
private func layoutViews() {
|
||||
donateTextLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
donateButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
donateTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
donateTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
donateTextLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
|
||||
donateButton.topAnchor.constraint(equalTo: donateTextLabel.bottomAnchor, constant: 10),
|
||||
donateButton.widthAnchor.constraint(equalTo: widthAnchor, constant: -40).withPriority(.defaultHigh),
|
||||
donateButton.widthAnchor.constraint(lessThanOrEqualToConstant: 400).withPriority(.defaultHigh),
|
||||
donateButton.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
donateButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
donateButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func donateButtonDidTap() {
|
||||
donateButtonDidTapHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSLayoutConstraint {
|
||||
func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
|
||||
self.priority = priority
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
final class InfoTableViewCell: UITableViewCell {
|
||||
|
||||
private let infoView = InfoView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: reuseIdentifier)
|
||||
setupView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
backgroundView = UIView() // Set background color to clear
|
||||
setStyle(.clearBackground)
|
||||
contentView.addSubview(infoView)
|
||||
infoView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
infoView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
infoView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
infoView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
infoView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func set(image: UIImage?, title: String) {
|
||||
infoView.set(image: image, title: title)
|
||||
}
|
||||
}
|
||||
81
iphone/Maps/UI/Help/AboutController/Views/InfoView.swift
Normal file
81
iphone/Maps/UI/Help/AboutController/Views/InfoView.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
final class InfoView: UIView {
|
||||
|
||||
private let stackView = UIStackView()
|
||||
private let imageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private lazy var imageViewWidthConstrain = imageView.widthAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
self.setupView()
|
||||
self.arrangeViews()
|
||||
self.layoutViews()
|
||||
}
|
||||
|
||||
convenience init(image: UIImage?, title: String) {
|
||||
self.init()
|
||||
self.set(image: image, title: title)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
if #available(iOS 13.0, *), traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
|
||||
imageView.applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fill
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 16
|
||||
|
||||
titleLabel.setFontStyle(.regular16, color: .blackPrimary)
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
titleLabel.numberOfLines = .zero
|
||||
|
||||
imageView.setStyle(.black)
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
private func arrangeViews() {
|
||||
addSubview(stackView)
|
||||
stackView.addArrangedSubview(imageView)
|
||||
stackView.addArrangedSubview(titleLabel)
|
||||
}
|
||||
|
||||
private func layoutViews() {
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
imageView.heightAnchor.constraint(equalToConstant: 24),
|
||||
imageViewWidthConstrain
|
||||
])
|
||||
updateImageWidth()
|
||||
}
|
||||
|
||||
private func updateImageWidth() {
|
||||
imageViewWidthConstrain.constant = imageView.image == nil ? 0 : 24
|
||||
imageView.isHidden = imageView.image == nil
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func set(image: UIImage?, title: String) {
|
||||
imageView.image = image
|
||||
titleLabel.text = title
|
||||
updateImageWidth()
|
||||
}
|
||||
}
|
||||
87
iphone/Maps/UI/Help/AboutController/Views/OSMView.swift
Normal file
87
iphone/Maps/UI/Help/AboutController/Views/OSMView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
final class OSMView: UIView {
|
||||
|
||||
private let OSMImageView = UIImageView()
|
||||
private let OSMTextLabel = UILabel()
|
||||
private var mapDate: String?
|
||||
|
||||
var didTapHandler: (() -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupViews()
|
||||
arrangeViews()
|
||||
layoutViews()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard let mapDate, traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
|
||||
OSMTextLabel.attributedText = attributedString(for: mapDate)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func setMapDate(_ mapDate: String) {
|
||||
self.mapDate = mapDate
|
||||
OSMTextLabel.attributedText = attributedString(for: mapDate)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private func setupViews() {
|
||||
OSMImageView.image = UIImage(named: "osm_logo")
|
||||
|
||||
OSMTextLabel.setFontStyle(.regular14, color: .blackPrimary)
|
||||
OSMTextLabel.lineBreakMode = .byWordWrapping
|
||||
OSMTextLabel.numberOfLines = 0
|
||||
OSMTextLabel.isUserInteractionEnabled = true
|
||||
|
||||
let osmDidTapGesture = UITapGestureRecognizer(target: self, action: #selector(osmDidTap))
|
||||
OSMTextLabel.addGestureRecognizer(osmDidTapGesture)
|
||||
}
|
||||
|
||||
private func arrangeViews() {
|
||||
addSubview(OSMImageView)
|
||||
addSubview(OSMTextLabel)
|
||||
}
|
||||
|
||||
private func layoutViews() {
|
||||
OSMImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
OSMTextLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
OSMImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
OSMImageView.heightAnchor.constraint(equalToConstant: 40),
|
||||
OSMImageView.widthAnchor.constraint(equalTo: OSMImageView.heightAnchor),
|
||||
OSMImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
|
||||
OSMImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
|
||||
|
||||
OSMTextLabel.leadingAnchor.constraint(equalTo: OSMImageView.trailingAnchor, constant: 8),
|
||||
OSMTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
OSMTextLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
|
||||
OSMTextLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
|
||||
OSMTextLabel.centerYAnchor.constraint(equalTo: OSMImageView.centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func osmDidTap() {
|
||||
didTapHandler?()
|
||||
}
|
||||
|
||||
private func attributedString(for date: String) -> NSAttributedString {
|
||||
let osmLink = "OpenStreetMap.org"
|
||||
let attributedString = NSMutableAttributedString(string: String(format: L("osm_presentation"), date.trimmingCharacters(in: .punctuationCharacters)),
|
||||
attributes: [.font: UIFont.regular14(),
|
||||
.foregroundColor: StyleManager.shared.theme!.colors.blackPrimaryText]
|
||||
)
|
||||
let linkRange = attributedString.mutableString.range(of: osmLink)
|
||||
attributedString.addAttribute(.link, value: "https://www.openstreetmap.org/", range: linkRange)
|
||||
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
final class SocialMediaCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
private let imageView = UIImageView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
|
||||
updateImageColor()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
setStyle(.clearBackground)
|
||||
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(imageView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
private func updateImageColor() {
|
||||
imageView.tintColor = StyleManager.shared.theme?.colors.blackPrimaryText
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func setImage(_ image: UIImage) {
|
||||
imageView.image = image
|
||||
updateImageColor()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
final class SocialMediaCollectionViewHeader: UICollectionReusableView {
|
||||
|
||||
static let reuseIdentifier = String(describing: SocialMediaCollectionViewHeader.self)
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
addSubview(titleLabel)
|
||||
titleLabel.setFontStyleAndApply(.regular16, color: .blackPrimary)
|
||||
titleLabel.numberOfLines = 1
|
||||
titleLabel.allowsDefaultTighteningForTruncation = true
|
||||
titleLabel.adjustsFontSizeToFitWidth = true
|
||||
titleLabel.minimumScaleFactor = 0.5
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func setTitle(_ title: String) {
|
||||
titleLabel.text = title
|
||||
}
|
||||
}
|
||||
24
iphone/Maps/UI/Help/FaqController.swift
Normal file
24
iphone/Maps/UI/Help/FaqController.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
final class FaqController: MWMViewController {
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
// TODO: FAQ?
|
||||
self.title = L("help")
|
||||
|
||||
let path = Bundle.main.path(forResource: "faq", ofType: "html")!
|
||||
let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
|
||||
let webViewController = WebViewController.init(html: html, baseUrl: nil, title: nil)!
|
||||
webViewController.openInSafari = true
|
||||
addChild(webViewController)
|
||||
let aboutView = webViewController.view!
|
||||
view.addSubview(aboutView)
|
||||
|
||||
aboutView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
aboutView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
aboutView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
aboutView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
aboutView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user