co-maps/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift

569 lines
21 KiB
Swift
Raw Normal View History

2025-11-22 13:58:55 +01:00
final class InfoItemView: UIView {
private enum Constants {
static let viewHeight: CGFloat = 44
static let stackViewSpacing: CGFloat = 0
static let iconButtonSize: CGFloat = 56
static let iconButtonEdgeInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
static let infoLabelFontSize: CGFloat = 16
static let infoLabelTopBottomSpacing: CGFloat = 10
static let accessoryButtonSize: CGFloat = 44
}
enum Style {
case regular
case link
}
typealias TapHandler = () -> Void
let iconButton = UIButton()
let infoLabel = UILabel()
let accessoryButton = UIButton()
var infoLabelTapHandler: TapHandler?
var infoLabelLongPressHandler: TapHandler?
var iconButtonTapHandler: TapHandler?
var accessoryImageTapHandler: TapHandler?
private var style: Style = .regular
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
layout()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
layout()
}
private func setupView() {
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onInfoLabelTap)))
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(onInfoLabelLongPress(_:))))
infoLabel.lineBreakMode = .byTruncatingTail
infoLabel.numberOfLines = 1
infoLabel.allowsDefaultTighteningForTruncation = true
infoLabel.isUserInteractionEnabled = false
iconButton.imageView?.contentMode = .scaleAspectFit
iconButton.addTarget(self, action: #selector(onIconButtonTap), for: .touchUpInside)
iconButton.contentEdgeInsets = Constants.iconButtonEdgeInsets
accessoryButton.addTarget(self, action: #selector(onAccessoryButtonTap), for: .touchUpInside)
}
private func layout() {
addSubview(iconButton)
addSubview(infoLabel)
addSubview(accessoryButton)
translatesAutoresizingMaskIntoConstraints = false
iconButton.translatesAutoresizingMaskIntoConstraints = false
infoLabel.translatesAutoresizingMaskIntoConstraints = false
accessoryButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: Constants.viewHeight),
iconButton.leadingAnchor.constraint(equalTo: leadingAnchor),
iconButton.centerYAnchor.constraint(equalTo: centerYAnchor),
iconButton.widthAnchor.constraint(equalToConstant: Constants.iconButtonSize),
iconButton.topAnchor.constraint(equalTo: topAnchor),
iconButton.bottomAnchor.constraint(equalTo: bottomAnchor),
infoLabel.leadingAnchor.constraint(equalTo: iconButton.trailingAnchor),
infoLabel.topAnchor.constraint(equalTo: topAnchor),
infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
infoLabel.trailingAnchor.constraint(equalTo: accessoryButton.leadingAnchor),
accessoryButton.trailingAnchor.constraint(equalTo: trailingAnchor),
accessoryButton.centerYAnchor.constraint(equalTo: centerYAnchor),
accessoryButton.widthAnchor.constraint(equalToConstant: Constants.accessoryButtonSize),
accessoryButton.topAnchor.constraint(equalTo: topAnchor),
accessoryButton.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
@objc
private func onInfoLabelTap() {
infoLabelTapHandler?()
}
@objc
private func onInfoLabelLongPress(_ sender: UILongPressGestureRecognizer) {
guard sender.state == .began else { return }
infoLabelLongPressHandler?()
}
@objc
private func onIconButtonTap() {
iconButtonTapHandler?()
}
@objc
private func onAccessoryButtonTap() {
accessoryImageTapHandler?()
}
func setStyle(_ style: Style) {
switch style {
case .regular:
iconButton.setStyleAndApply(.black)
infoLabel.setFontStyleAndApply(.regular16, color: .blackPrimary)
case .link:
iconButton.setStyleAndApply(.blue)
infoLabel.setFontStyleAndApply(.regular16, color: .linkBlue)
}
accessoryButton.setStyleAndApply(.black)
self.style = style
}
func setAccessory(image: UIImage?, tapHandler: TapHandler? = nil) {
accessoryButton.setTitle("", for: .normal)
accessoryButton.setImage(image, for: .normal)
accessoryButton.isHidden = image == nil
accessoryImageTapHandler = tapHandler
}
}
protocol PlacePageInfoViewControllerDelegate: AnyObject {
var shouldShowOpenInApp: Bool { get }
func didPressCall(to phone: PlacePagePhone)
func didPressWebsite()
func didPressWebsiteMenu()
func didPressWikipedia()
func didPressWikimediaCommons()
func didPressFediverse()
func didPressFacebook()
func didPressInstagram()
func didPressTwitter()
func didPressVk()
func didPressLine()
func didPressBluesky()
func didPressPanoramax()
func didPressEmail()
func didPressOpenInApp(from sourceView: UIView)
func didCopy(_ content: String)
}
class PlacePageInfoViewController: UIViewController {
private struct Constants {
static let coordFormatIdKey = "PlacePageInfoViewController_coordFormatIdKey"
}
private typealias TapHandler = InfoItemView.TapHandler
private typealias Style = InfoItemView.Style
@IBOutlet var stackView: UIStackView!
@IBOutlet var checkDateLabel: UILabel!
@IBOutlet var checkDateLabelLayoutConstraint: NSLayoutConstraint!
private lazy var openingHoursViewController: OpeningHoursViewController = {
storyboard!.instantiateViewController(ofType: OpeningHoursViewController.self)
}()
private var rawOpeningHoursView: InfoItemView?
private var phoneViews: [InfoItemView] = []
private var websiteView: InfoItemView?
private var websiteMenuView: InfoItemView?
private var wikipediaView: InfoItemView?
private var wikimediaCommonsView: InfoItemView?
private var emailView: InfoItemView?
private var fediverseView: InfoItemView?
private var facebookView: InfoItemView?
private var instagramView: InfoItemView?
private var twitterView: InfoItemView?
private var vkView: InfoItemView?
private var lineView: InfoItemView?
private var blueskyView: InfoItemView?
private var panoramaxView: InfoItemView?
private var cuisineView: InfoItemView?
private var operatorView: InfoItemView?
private var wifiView: InfoItemView?
private var atmView: InfoItemView?
private var addressView: InfoItemView?
private var levelView: InfoItemView?
private var coordinatesView: InfoItemView?
private var openWithAppView: InfoItemView?
private var capacityView: InfoItemView?
private var wheelchairView: InfoItemView?
private var selfServiceView: InfoItemView?
private var outdoorSeatingView: InfoItemView?
private var driveThroughView: InfoItemView?
private var networkView: InfoItemView?
weak var placePageInfoData: PlacePageInfoData!
weak var delegate: PlacePageInfoViewControllerDelegate?
var coordinatesFormatId: Int {
get { UserDefaults.standard.integer(forKey: Constants.coordFormatIdKey) }
set { UserDefaults.standard.set(newValue, forKey: Constants.coordFormatIdKey) }
}
override func viewDidLoad() {
super.viewDidLoad()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.addSeparator(.bottom)
view.addSubview(stackView)
setupViews()
}
// MARK: private
private func setupViews() {
if let openingHours = placePageInfoData.openingHours {
openingHoursViewController.openingHours = openingHours
openingHoursViewController.openingHoursCheckDate = placePageInfoData.checkDateOpeningHours
addChild(openingHoursViewController)
addToStack(openingHoursViewController.view)
openingHoursViewController.didMove(toParent: self)
} else if let openingHoursString = placePageInfoData.openingHoursString {
rawOpeningHoursView = createInfoItem(openingHoursString, icon: UIImage(named: "ic_placepage_open_hours"))
rawOpeningHoursView?.infoLabel.numberOfLines = 0
}
if let cuisine = placePageInfoData.cuisine {
cuisineView = createInfoItem(cuisine, icon: UIImage(named: "ic_placepage_cuisine"))
}
/// @todo Entrance is missing compared with Android. It's shown in title, but anyway ..
phoneViews = placePageInfoData.phones.map({ phone in
var cellStyle: Style = .regular
if let phoneUrl = phone.url, UIApplication.shared.canOpenURL(phoneUrl) {
cellStyle = .link
}
return createInfoItem(phone.phone,
icon: UIImage(named: "ic_placepage_phone_number"),
style: cellStyle,
tapHandler: { [weak self] in
self?.delegate?.didPressCall(to: phone)
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(phone.phone)
})
})
if let ppOperator = placePageInfoData.ppOperator {
operatorView = createInfoItem(ppOperator, icon: UIImage(named: "ic_placepage_operator"))
}
if let network = placePageInfoData.network {
networkView = createInfoItem(network, icon: UIImage(named: "ic_placepage_network"))
}
if let website = placePageInfoData.website {
// Strip website url only when the value is displayed, to avoid issues when it's opened or edited.
websiteView = createInfoItem(stripUrl(str: website),
icon: UIImage(named: "ic_placepage_website"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressWebsite()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(website)
})
}
if let websiteMenu = placePageInfoData.websiteMenu {
websiteView = createInfoItem(L("website_menu"),
icon: UIImage(named: "ic_placepage_website_menu"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressWebsiteMenu()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(websiteMenu)
})
}
if let wikipedia = placePageInfoData.wikipedia {
wikipediaView = createInfoItem(L("read_in_wikipedia"),
icon: UIImage(named: "ic_placepage_wiki"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressWikipedia()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(wikipedia)
})
}
if let wikimediaCommons = placePageInfoData.wikimediaCommons {
wikimediaCommonsView = createInfoItem(L("wikimedia_commons"),
icon: UIImage(named: "ic_placepage_wikimedia_commons"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressWikimediaCommons()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(wikimediaCommons)
})
}
if let wifi = placePageInfoData.wifiAvailable {
wifiView = createInfoItem(wifi, icon: UIImage(named: "ic_placepage_wifi"))
}
if let atm = placePageInfoData.atm {
atmView = createInfoItem(atm, icon: UIImage(named: "ic_placepage_atm"))
}
if let level = placePageInfoData.level {
levelView = createInfoItem(level, icon: UIImage(named: "ic_placepage_level"))
}
if let capacity = placePageInfoData.capacity {
capacityView = createInfoItem(capacity, icon: UIImage(named: "ic_placepage_capacity"))
}
if let wheelchair = placePageInfoData.wheelchair {
wheelchairView = createInfoItem(wheelchair, icon: UIImage(named: "ic_placepage_wheelchair"))
}
if let selfService = placePageInfoData.selfService {
selfServiceView = createInfoItem(selfService, icon: UIImage(named: "ic_placepage_self_service"))
}
if let outdoorSeating = placePageInfoData.outdoorSeating {
outdoorSeatingView = createInfoItem(outdoorSeating, icon: UIImage(named: "ic_placepage_outdoor_seating"))
}
if let driveThrough = placePageInfoData.driveThrough {
driveThroughView = createInfoItem(driveThrough, icon: UIImage(named: "ic_placepage_drive_through"))
}
if let email = placePageInfoData.email {
emailView = createInfoItem(email,
icon: UIImage(named: "ic_placepage_email"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressEmail()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(email)
})
}
if let fediverse = placePageInfoData.fediverse {
fediverseView = createInfoItem(fediverse,
icon: UIImage(named: "ic_placepage_fediverse"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressFediverse()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(fediverse)
})
}
if let facebook = placePageInfoData.facebook {
facebookView = createInfoItem(facebook,
icon: UIImage(named: "ic_placepage_facebook"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressFacebook()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(facebook)
})
}
if let instagram = placePageInfoData.instagram {
instagramView = createInfoItem(instagram,
icon: UIImage(named: "ic_placepage_instagram"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressInstagram()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(instagram)
})
}
if let twitter = placePageInfoData.twitter {
twitterView = createInfoItem(twitter,
icon: UIImage(named: "ic_placepage_twitter"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressTwitter()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(twitter)
})
}
if let vk = placePageInfoData.vk {
vkView = createInfoItem(vk,
icon: UIImage(named: "ic_placepage_vk"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressVk()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(vk)
})
}
if let line = placePageInfoData.line {
lineView = createInfoItem(line,
icon: UIImage(named: "ic_placepage_line"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressLine()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(line)
})
}
if let bluesky = placePageInfoData.bluesky {
blueskyView = createInfoItem(bluesky,
icon: UIImage(named: "ic_placepage_bluesky"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressBluesky()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(bluesky)
})
}
if let panoramax = placePageInfoData.panoramax {
panoramaxView = createInfoItem(L("panoramax_picture"),
icon: UIImage(named: "ic_placepage_panoramax"),
style: .link,
tapHandler: { [weak self] in
self?.delegate?.didPressPanoramax()
},
longPressHandler: { [weak self] in
self?.delegate?.didCopy(panoramax)
})
}
if let address = placePageInfoData.address {
addressView = createInfoItem(address,
icon: UIImage(named: "ic_placepage_address"),
longPressHandler: { [weak self] in
self?.delegate?.didCopy(address)
})
}
setupCoordinatesView()
setupOpenWithAppView()
if let checkDate = placePageInfoData.checkDate {
let checkDateFormatter = RelativeDateTimeFormatter()
checkDateFormatter.unitsStyle = .spellOut
checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now)
self.checkDateLabel.text = String(format: L("existence_confirmed_time_ago"), checkDateFormatter.localizedString(for: checkDate, relativeTo: Date.now))
checkDateLabel.isHidden = false
NSLayoutConstraint.activate([checkDateLabelLayoutConstraint])
} else {
checkDateLabel.text = String()
checkDateLabel.isHidden = true
NSLayoutConstraint.deactivate([checkDateLabelLayoutConstraint])
}
}
private func setupCoordinatesView() {
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
var formatId = coordinatesFormatId
if formatId >= coordFormats.count {
formatId = 0
}
coordinatesView = createInfoItem(coordFormats[formatId],
icon: UIImage(named: "ic_placepage_coordinate"),
accessoryImage: UIImage(named: "ic_placepage_change"),
tapHandler: { [weak self] in
guard let self else { return }
let formatId = (self.coordinatesFormatId + 1) % coordFormats.count
self.setCoordinatesSelected(formatId: formatId)
},
longPressHandler: { [weak self] in
self?.copyCoordinatesToPasteboard()
})
let menu = UIMenu(children: coordFormats.enumerated().map { (index, format) in
UIAction(title: format, handler: { [weak self] _ in
self?.setCoordinatesSelected(formatId: index)
self?.copyCoordinatesToPasteboard()
})
})
coordinatesView?.accessoryButton.menu = menu
coordinatesView?.accessoryButton.showsMenuAsPrimaryAction = true
}
private func setCoordinatesSelected(formatId: Int) {
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
coordinatesFormatId = formatId
let coordinates: String = coordFormats[formatId]
coordinatesView?.infoLabel.text = coordinates
}
private func copyCoordinatesToPasteboard() {
guard let coordFormats = placePageInfoData.coordFormats as? Array<String> else { return }
let coordinates: String = coordFormats[coordinatesFormatId]
delegate?.didCopy(coordinates)
}
private func setupOpenWithAppView() {
guard let delegate, delegate.shouldShowOpenInApp else { return }
openWithAppView = createInfoItem(L("open_in_app"),
icon: UIImage(named: "ic_open_in_app"),
style: .link,
tapHandler: { [weak self] in
guard let self, let openWithAppView else { return }
self.delegate?.didPressOpenInApp(from: openWithAppView)
})
}
private func createInfoItem(_ info: String,
icon: UIImage?,
tapIconHandler: TapHandler? = nil,
style: Style = .regular,
accessoryImage: UIImage? = nil,
tapHandler: TapHandler? = nil,
longPressHandler: TapHandler? = nil,
accessoryImageTapHandler: TapHandler? = nil) -> InfoItemView {
let view = InfoItemView()
addToStack(view)
view.iconButton.setImage(icon?.withRenderingMode(.alwaysTemplate), for: .normal)
view.iconButtonTapHandler = tapIconHandler
view.infoLabel.text = info
view.setStyle(style)
view.infoLabelTapHandler = tapHandler
view.infoLabelLongPressHandler = longPressHandler
view.setAccessory(image: accessoryImage, tapHandler: accessoryImageTapHandler)
return view
}
private func addToStack(_ view: UIView) {
stackView.addArrangedSubviewWithSeparator(view, insets: UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0))
}
private static let kHttp = "http://"
private static let kHttps = "https://"
private func stripUrl(str: String) -> String {
let dropFromStart = str.hasPrefix(PlacePageInfoViewController.kHttps) ? PlacePageInfoViewController.kHttps.count
: (str.hasPrefix(PlacePageInfoViewController.kHttp) ? PlacePageInfoViewController.kHttp.count : 0);
let dropFromEnd = str.hasSuffix("/") ? 1 : 0;
return String(str.dropFirst(dropFromStart).dropLast(dropFromEnd))
}
}
private extension UIStackView {
func addArrangedSubviewWithSeparator(_ view: UIView, insets: UIEdgeInsets = .zero) {
if !arrangedSubviews.isEmpty {
view.addSeparator(thickness: CGFloat(1.0), insets: insets)
}
addArrangedSubview(view)
}
}