Files
comaps/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift
Eugene Nikolsky 50e6376afd [ios] Support multiple phone numbers
Multiple phone numbers should be [separated with `;`][parsing] in OSM
`phone` values. This commit adds support for parsing and displaying such
phone numbers individually. Example POI with three phone numbers:
https://www.openstreetmap.org/way/233417266.

Before this change, the phone was displayed as one
value and trying to call it would fail because all the digits were
concatenated together, resulting in an invalid number. For the POI
above, the program tried to call
`tel://+150332563111503325879018008756807`.

This change fixes the parsing of `FMD_PHONE_NUMBER` into an array of
phone numbers. That required updates in a few areas:

- the POI details view (`PlacePageInfoViewController`) now displays
  every phone number as a separate row, each with a clickable link for
  that number; long-click to copy also works.

- the share info preparation (`MWMShareActivityItem`) displays phone
  numbers separated with `; `, which provides a better phone detection
  for iOS.

- the Call button (`PlacePageInteractor`) now has to ask the user which
  number to call if there are more than one.

I tested this on an iPhone 15 Pro, iOS 17.2 simulator (temporarily
commenting the "can make phone call" checks).

Note: the Editing screen wasn't updated in order to keep this PR
smaller.

Fixes https://git.omaps.dev/organicmaps/organicmaps/issues/2458. The
corresponding fix for Android was in
https://github.com/organicmaps/organicmaps/pull/845.

[parsing]: https://wiki.openstreetmap.org/wiki/Key:phone#Parsing_phone_numbers

Signed-off-by: Eugene Nikolsky <omaps@egeek.me>
2025-05-19 10:52:42 +02:00

226 lines
6.8 KiB
Swift

protocol ActionBarViewControllerDelegate: AnyObject {
func actionBar(_ actionBar: ActionBarViewController, didPressButton type: ActionBarButtonType)
}
final class ActionBarViewController: UIViewController {
@IBOutlet var stackView: UIStackView!
private(set) var downloadButton: ActionBarButton? = nil
private(set) var bookmarkButton: ActionBarButton? = nil
private var popoverSourceView: UIView? {
stackView.arrangedSubviews.last
}
var placePageData: PlacePageData!
var isRoutePlanning = false
var canAddStop = false
private var visibleButtons: [ActionBarButtonType] = []
private var additionalButtons: [ActionBarButtonType] = []
weak var delegate: ActionBarViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
configureButtons()
}
// MARK: - Private methods
private func configureButtons() {
if placePageData.isRoutePoint {
visibleButtons.append(.routeRemoveStop)
} else if placePageData.roadType != .none {
switch placePageData.roadType {
case .toll:
visibleButtons.append(.avoidToll)
case .ferry:
visibleButtons.append(.avoidFerry)
case .dirty:
visibleButtons.append(.avoidDirty)
default:
fatalError()
}
} else {
configButton1()
configButton2()
configButton3()
configButton4()
}
setupButtonsState()
}
private func configButton1() {
if let mapNodeAttributes = placePageData.mapNodeAttributes {
switch mapNodeAttributes.nodeStatus {
case .onDiskOutOfDate, .onDisk, .undefined:
break
case .downloading, .applying, .inQueue, .error, .notDownloaded, .partly:
visibleButtons.append(.download)
return
@unknown default:
fatalError()
}
}
var buttons: [ActionBarButtonType] = []
if isRoutePlanning {
buttons.append(.routeFrom)
}
let hasAnyPhones = !(placePageData.infoData?.phones ?? []).isEmpty
if hasAnyPhones, AppInfo.shared().canMakeCalls {
buttons.append(.call)
}
if !isRoutePlanning {
buttons.append(.routeFrom)
}
assert(buttons.count > 0)
visibleButtons.append(buttons[0])
if buttons.count > 1 {
additionalButtons.append(contentsOf: buttons.suffix(from: 1))
}
}
private func configButton2() {
var buttons: [ActionBarButtonType] = []
if canAddStop {
buttons.append(.routeAddStop)
}
switch placePageData.objectType {
case .POI, .bookmark:
buttons.append(.bookmark)
case .track:
buttons.append(.track)
case .trackRecording:
// TODO: implement for track recording
break
@unknown default:
fatalError()
}
assert(buttons.count > 0)
visibleButtons.append(buttons[0])
if buttons.count > 1 {
additionalButtons.append(contentsOf: buttons.suffix(from: 1))
}
}
private func configButton3() {
visibleButtons.append(.routeTo)
}
private func configButton4() {
guard !additionalButtons.isEmpty else { return }
additionalButtons.count == 1 ? visibleButtons.append(additionalButtons[0]) : visibleButtons.append(.more)
}
private func setupButtonsState() {
for buttonType in visibleButtons {
let (selected, enabled) = buttonState(buttonType)
let button = ActionBarButton(delegate: self,
buttonType: buttonType,
isSelected: selected,
isEnabled: enabled)
stackView.addArrangedSubview(button)
switch buttonType {
case .download:
downloadButton = button
updateDownloadButtonState(placePageData.mapNodeAttributes!.nodeStatus)
case .bookmark:
bookmarkButton = button
default:
break
}
}
}
private func buttonState(_ buttonType: ActionBarButtonType) -> (selected: Bool, enabled: Bool) {
var selected = false
let enabled = true
switch buttonType {
case .bookmark:
selected = placePageData.bookmarkData != nil
case .track:
selected = placePageData.trackData != nil
default:
break
}
return (selected, enabled)
}
private func showMore() {
let actionSheet = UIAlertController(title: placePageData.previewData.title,
message: placePageData.previewData.subtitle,
preferredStyle: .actionSheet)
for button in additionalButtons {
let (selected, enabled) = buttonState(button)
let action = UIAlertAction(title: titleForButton(button, selected),
style: .default,
handler: { [weak self] _ in
guard let self = self else { return }
self.delegate?.actionBar(self, didPressButton: button)
})
action.isEnabled = enabled
actionSheet.addAction(action)
}
actionSheet.addAction(UIAlertAction(title: L("cancel"), style: .cancel))
if let popover = actionSheet.popoverPresentationController, let sourceView = stackView.arrangedSubviews.last {
popover.sourceView = sourceView
popover.sourceRect = sourceView.bounds
}
present(actionSheet, animated: true)
}
// MARK: - Public methods
func resetButtons() {
stackView.arrangedSubviews.forEach {
stackView.removeArrangedSubview($0)
$0.removeFromSuperview()
}
visibleButtons.removeAll()
additionalButtons.removeAll()
downloadButton = nil
bookmarkButton = nil
configureButtons()
}
func updateDownloadButtonState(_ nodeStatus: MapNodeStatus) {
guard let downloadButton = downloadButton, let mapNodeAttributes = placePageData.mapNodeAttributes else { return }
switch mapNodeAttributes.nodeStatus {
case .downloading:
downloadButton.mapDownloadProgress?.state = .progress
case .applying, .inQueue:
downloadButton.mapDownloadProgress?.state = .spinner
case .error:
downloadButton.mapDownloadProgress?.state = .failed
case .onDisk, .undefined, .onDiskOutOfDate:
downloadButton.mapDownloadProgress?.state = .completed
case .notDownloaded, .partly:
downloadButton.mapDownloadProgress?.state = .normal
@unknown default:
fatalError()
}
}
func updateBookmarkButtonState(isSelected: Bool) {
guard let bookmarkButton else { return }
if !isSelected && BookmarksManager.shared().hasRecentlyDeletedBookmark() {
bookmarkButton.setBookmarkButtonState(.recover)
return
}
bookmarkButton.setBookmarkButtonState(isSelected ? .delete : .save)
}
}
extension ActionBarViewController: ActionBarButtonDelegate {
func tapOnButton(with type: ActionBarButtonType) {
switch type {
case .more:
showMore()
default:
delegate?.actionBar(self, didPressButton: type)
}
}
}