[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>
This commit is contained in:
Eugene Nikolsky
2025-04-02 20:46:13 +03:00
committed by Konstantin Pastbin
parent a3ba5c53b6
commit 50e6376afd
6 changed files with 47 additions and 15 deletions

View File

@@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly, nullable) NSString *openingHoursString; @property(nonatomic, readonly, nullable) NSString *openingHoursString;
@property(nonatomic, readonly, nullable) OpeningHours *openingHours; @property(nonatomic, readonly, nullable) OpeningHours *openingHours;
@property(nonatomic, readonly, nullable) PlacePagePhone *phone; @property(nonatomic, readonly) NSArray<PlacePagePhone *> *phones;
@property(nonatomic, readonly, nullable) NSString *website; @property(nonatomic, readonly, nullable) NSString *website;
@property(nonatomic, readonly, nullable) NSString *wikipedia; @property(nonatomic, readonly, nullable) NSString *wikipedia;
@property(nonatomic, readonly, nullable) NSString *wikimediaCommons; @property(nonatomic, readonly, nullable) NSString *wikimediaCommons;

View File

@@ -47,14 +47,18 @@ NSString * GetLocalizedMetadataValueString(MapObject::MetadataID metaID, std::st
break; break;
case MetadataID::FMD_PHONE_NUMBER: case MetadataID::FMD_PHONE_NUMBER:
{ {
NSString *phone = ToNSString(value); NSArray<NSString *> *phones = [ToNSString(value) componentsSeparatedByString:@";"];
NSString *filteredDigits = [[phone componentsSeparatedByCharactersInSet: NSMutableArray<PlacePagePhone *> *placePhones = [NSMutableArray new];
[[NSCharacterSet decimalDigitCharacterSet] invertedSet]] [phones enumerateObjectsUsingBlock:^(NSString * _Nonnull phone, NSUInteger idx, BOOL * _Nonnull stop) {
componentsJoinedByString:@""]; NSString *filteredDigits = [[phone componentsSeparatedByCharactersInSet:
NSString *resultNumber = [phone hasPrefix:@"+"] ? [NSString stringWithFormat:@"+%@", filteredDigits] : filteredDigits; [[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
NSURL *phoneUrl = [NSURL URLWithString:[NSString stringWithFormat:@"tel://%@", resultNumber]]; componentsJoinedByString:@""];
NSString *resultNumber = [phone hasPrefix:@"+"] ? [NSString stringWithFormat:@"+%@", filteredDigits] : filteredDigits;
NSURL *phoneUrl = [NSURL URLWithString:[NSString stringWithFormat:@"tel://%@", resultNumber]];
_phone = [PlacePagePhone placePagePhoneWithPhone:phone andURL:phoneUrl]; [placePhones addObject:[PlacePagePhone placePagePhoneWithPhone:phone andURL:phoneUrl]];
}];
_phones = [placePhones copy];
break; break;
} }
case MetadataID::FMD_WEBSITE: _website = ToNSString(value); break; case MetadataID::FMD_WEBSITE: _website = ToNSString(value); break;

View File

@@ -136,11 +136,16 @@ NSString * httpGe0Url(NSString * shortUrl)
stringWithFormat:@"%@ %@\n%@", L(@"my_position_share_email_subject"), url, ge0Url]; stringWithFormat:@"%@ %@\n%@", L(@"my_position_share_email_subject"), url, ge0Url];
} }
NSMutableArray *phones = [NSMutableArray new];
[self.data.infoData.phones enumerateObjectsUsingBlock:^(PlacePagePhone * _Nonnull phone, NSUInteger idx, BOOL * _Nonnull stop) {
[phones addObject:phone.phone];
}];
NSMutableString * result = [L(@"sharing_call_action_look") mutableCopy]; NSMutableString * result = [L(@"sharing_call_action_look") mutableCopy];
std::vector<NSString *> strings{self.data.previewData.title, std::vector<NSString *> strings{self.data.previewData.title,
self.data.previewData.subtitle, self.data.previewData.subtitle,
self.data.previewData.secondarySubtitle, self.data.previewData.secondarySubtitle,
self.data.infoData.phone.phone, [phones componentsJoinedByString:@"; "],
url, url,
ge0Url}; ge0Url};

View File

@@ -66,7 +66,8 @@ final class ActionBarViewController: UIViewController {
if isRoutePlanning { if isRoutePlanning {
buttons.append(.routeFrom) buttons.append(.routeFrom)
} }
if placePageData.infoData?.phone != nil, AppInfo.shared().canMakeCalls { let hasAnyPhones = !(placePageData.infoData?.phones ?? []).isEmpty
if hasAnyPhones, AppInfo.shared().canMakeCalls {
buttons.append(.call) buttons.append(.call)
} }
if !isRoutePlanning { if !isRoutePlanning {

View File

@@ -103,7 +103,7 @@ class PlacePageInfoViewController: UIViewController {
}() }()
private var rawOpeningHoursView: InfoItemViewController? private var rawOpeningHoursView: InfoItemViewController?
private var phoneView: InfoItemViewController? private var phoneViews: [InfoItemViewController] = []
private var websiteView: InfoItemViewController? private var websiteView: InfoItemViewController?
private var websiteMenuView: InfoItemViewController? private var websiteMenuView: InfoItemViewController?
private var wikipediaView: InfoItemViewController? private var wikipediaView: InfoItemViewController?
@@ -157,12 +157,12 @@ class PlacePageInfoViewController: UIViewController {
/// @todo Entrance is missing compared with Android. It's shown in title, but anyway .. /// @todo Entrance is missing compared with Android. It's shown in title, but anyway ..
if let phone = placePageInfoData.phone { phoneViews = placePageInfoData.phones.map({ phone in
var cellStyle: Style = .regular var cellStyle: Style = .regular
if let phoneUrl = phone.url, UIApplication.shared.canOpenURL(phoneUrl) { if let phoneUrl = phone.url, UIApplication.shared.canOpenURL(phoneUrl) {
cellStyle = .link cellStyle = .link
} }
phoneView = createInfoItem(phone.phone, return createInfoItem(phone.phone,
icon: UIImage(named: "ic_placepage_phone_number"), icon: UIImage(named: "ic_placepage_phone_number"),
style: cellStyle, style: cellStyle,
tapHandler: { [weak self] in tapHandler: { [weak self] in
@@ -171,7 +171,7 @@ class PlacePageInfoViewController: UIViewController {
longPressHandler: { [weak self] in longPressHandler: { [weak self] in
self?.delegate?.didCopy(phone.phone) self?.delegate?.didCopy(phone.phone)
}) })
} })
if let ppOperator = placePageInfoData.ppOperator { if let ppOperator = placePageInfoData.ppOperator {
operatorView = createInfoItem(ppOperator, icon: UIImage(named: "ic_placepage_operator")) operatorView = createInfoItem(ppOperator, icon: UIImage(named: "ic_placepage_operator"))

View File

@@ -188,7 +188,15 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate {
MWMPlacePageManagerHelper.addBookmark(placePageData) MWMPlacePageManagerHelper.addBookmark(placePageData)
} }
case .call: case .call:
MWMPlacePageManagerHelper.call(placePageData.infoData?.phone) // since `.call` is a case in an obj-c enum, it can't have associated data, so there is no easy way to
// pass the exact phone, and we have to ask the user here which one to use, if there are multiple ones
let phones = placePageData.infoData?.phones ?? []
let hasOnePhoneNumber = phones.count == 1
if hasOnePhoneNumber {
MWMPlacePageManagerHelper.call(phones[0])
} else if (phones.count > 1) {
showPhoneNumberPicker(phones, handler: MWMPlacePageManagerHelper.call)
}
case .download: case .download:
guard let mapNodeAttributes = placePageData.mapNodeAttributes else { guard let mapNodeAttributes = placePageData.mapNodeAttributes else {
fatalError("Download button can't be displayed if mapNodeAttributes is empty") fatalError("Download button can't be displayed if mapNodeAttributes is empty")
@@ -251,6 +259,20 @@ extension PlacePageInteractor: ActionBarViewControllerDelegate {
} }
viewController.present(alert, animated: true) viewController.present(alert, animated: true)
} }
private func showPhoneNumberPicker(_ phones: [PlacePagePhone], handler: @escaping (PlacePagePhone) -> Void) {
guard let viewController else { return }
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
phones.forEach({phone in
alert.addAction(UIAlertAction(title: phone.phone, style: .default, handler: { _ in
handler(phone)
}))
})
alert.addAction(UIAlertAction(title: L("cancel"), style: .cancel))
viewController.present(alert, animated: true)
}
} }
// MARK: - ElevationProfileViewControllerDelegate // MARK: - ElevationProfileViewControllerDelegate