[iOS] Add Existence and Opening Hour confirmation to Place Page

Signed-off-by: eisa01 <your.email@example.com>
This commit is contained in:
Eivind Samseth
2025-08-08 19:19:03 +02:00
committed by Yannik Bloscheck
parent 795fe0ee09
commit d1f9806901
11 changed files with 114 additions and 9 deletions

View File

@@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface PlacePageInfoData : NSObject
@property(nonatomic, readonly, nullable) NSString *openingHoursString;
@property(nonatomic, readonly, nullable) NSDate *checkDate;
@property(nonatomic, readonly, nullable) NSDate *checkDateOpeningHours;
@property(nonatomic, readonly, nullable) OpeningHours *openingHours;
@property(nonatomic, readonly) NSArray<PlacePagePhone *> *phones;
@property(nonatomic, readonly, nullable) NSString *website;
@@ -38,6 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly, nullable) NSString *outdoorSeating;
@property(nonatomic, readonly, nullable) NSString *network;
- (NSDate * _Nullable)getMostRecentCheckDate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -20,8 +20,41 @@ NSString * GetLocalizedMetadataValueString(MapObject::MetadataID metaID, std::st
return ToNSString(platform::GetLocalizedTypeName(feature::ToString(metaID) + "." + value));
}
/// Parse date string in YYYY-MM-DD format to NSDate
NSDate * _Nullable ParseDateString(NSString * _Nullable dateString) {
if (!dateString || dateString.length == 0) {
return nil;
}
static NSDateFormatter *dateFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy-MM-dd";
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
});
return [dateFormatter dateFromString:dateString];
}
@implementation PlacePageInfoData
- (NSDate * _Nullable)getMostRecentCheckDate {
// CheckDate can utilize checkDateOpeningHours if that is available
// As surveying opening hours would confirm presence
if (_checkDate && _checkDateOpeningHours) {
// Both available - return the more recent date
return [_checkDate compare:_checkDateOpeningHours] == NSOrderedDescending ? _checkDate : _checkDateOpeningHours;
} else if (_checkDate) {
return _checkDate;
} else if (_checkDateOpeningHours) {
return _checkDateOpeningHours;
} else {
return nil;
}
}
@end
@implementation PlacePageInfoData (Core)
@@ -45,6 +78,12 @@ NSString * GetLocalizedMetadataValueString(MapObject::MetadataID metaID, std::st
_openingHours = [[OpeningHours alloc] initWithRawString:_openingHoursString
localization:localization];
break;
case MetadataID::FMD_CHECK_DATE:
_checkDate = ParseDateString(ToNSString(value));
break;
case MetadataID::FMD_CHECK_DATE_OPEN_HOURS:
_checkDateOpeningHours = ParseDateString(ToNSString(value));
break;
case MetadataID::FMD_PHONE_NUMBER:
{
NSArray<NSString *> *phones = [ToNSString(value) componentsSeparatedByString:@";"];

View File

@@ -0,0 +1,13 @@
import Foundation
extension Date {
func formatTimeAgo() -> String {
return Self.relativeFormatter.localizedString(for: self, relativeTo: Date())
}
private static let relativeFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter
}()
}

View File

@@ -552,6 +552,12 @@
"editor_report_problem_send_button" = "Send";
"autodownload" = "Auto-download maps";
/* Place page confirmation messages and time ago formatting */
"existence_confirmed_time_ago" = "Existence confirmed %@";
"hours_confirmed_time_ago" = "Confirmed %@";
/* Place Page opening hours text */
"closed_now" = "Closed now";

View File

@@ -321,6 +321,7 @@
676507601C10559800830BB3 /* patterns.txt in Resources */ = {isa = PBXBuildFile; fileRef = 451950391B7A3E070085DA05 /* patterns.txt */; };
676507611C10559B00830BB3 /* colors.txt in Resources */ = {isa = PBXBuildFile; fileRef = 452FCA3A1B6A3DF7007019AB /* colors.txt */; };
6B9978361C89A316003B8AA0 /* editor.config in Resources */ = {isa = PBXBuildFile; fileRef = 6B9978341C89A316003B8AA0 /* editor.config */; };
8325C4E12E45519600457516 /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8325C4E02E45519600457516 /* Date+TimeAgo.swift */; };
8C4FB9C72BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4FB9C62BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift */; };
8CB13C3B2BF1276A004288F2 /* CarplayPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB13C3A2BF1276A004288F2 /* CarplayPlaceholderView.swift */; };
99012847243F0D6900C72B10 /* UIViewController+alternative.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99012846243F0D6900C72B10 /* UIViewController+alternative.swift */; };
@@ -1208,6 +1209,7 @@
5605022E1B6211E100169CAD /* sound-strings */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "sound-strings"; path = "../../data/sound-strings"; sourceTree = "<group>"; };
6741AA5D1BF340DE002C974C /* CoMaps (Debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CoMaps (Debug).app"; sourceTree = BUILT_PRODUCTS_DIR; };
6B9978341C89A316003B8AA0 /* editor.config */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = editor.config; path = ../../data/editor.config; sourceTree = "<group>"; };
8325C4E02E45519600457516 /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
8C4FB9C62BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayWindowScaleAdjuster.swift; sourceTree = "<group>"; };
8CB13C3A2BF1276A004288F2 /* CarplayPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarplayPlaceholderView.swift; sourceTree = "<group>"; };
8D1107310486CEB800E47090 /* CoMaps.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CoMaps.plist; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = "<group>"; };
@@ -2285,6 +2287,7 @@
3454D7981E07F045004AF2AD /* Categories */ = {
isa = PBXGroup;
children = (
8325C4E02E45519600457516 /* Date+TimeAgo.swift */,
ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */,
99A614E223CDD1D900D8D8D0 /* UIButton+RuntimeAttributes.h */,
99A614E323CDD1D900D8D8D0 /* UIButton+RuntimeAttributes.m */,
@@ -4637,6 +4640,7 @@
998927382449E60200260CE2 /* BottomMenuPresenter.swift in Sources */,
27697F832E254AA100FBD913 /* EmbeddedSafariView.swift in Sources */,
F6E2FE821E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.m in Sources */,
8325C4E12E45519600457516 /* Date+TimeAgo.swift in Sources */,
F6E2FD6B1E097BA00083EBEC /* MWMMapDownloaderSubplaceTableViewCell.m in Sources */,
CDCA27842245090900167D87 /* ListenerContainer.swift in Sources */,
27AF18582E1DB63A00CD41E2 /* Appearance.swift in Sources */,

View File

@@ -5,8 +5,7 @@ class PlacePageHeaderBuilder {
let storyboard = UIStoryboard.instance(.placePage)
let viewController = storyboard.instantiateViewController(ofType: PlacePageHeaderViewController.self);
let presenter = PlacePageHeaderPresenter(view: viewController,
placePagePreviewData: data.previewData,
objectType: data.objectType,
placePageData: data,
delegate: delegate,
headerType: headerType)

View File

@@ -23,26 +23,28 @@ class PlacePageHeaderPresenter {
private weak var view: PlacePageHeaderViewProtocol?
private let placePagePreviewData: PlacePagePreviewData
private let placePageData: PlacePageData
let objectType: PlacePageObjectType
private weak var delegate: PlacePageHeaderViewControllerDelegate?
private let headerType: HeaderType
init(view: PlacePageHeaderViewProtocol,
placePagePreviewData: PlacePagePreviewData,
objectType: PlacePageObjectType,
placePageData: PlacePageData,
delegate: PlacePageHeaderViewControllerDelegate?,
headerType: HeaderType) {
self.view = view
self.delegate = delegate
self.placePagePreviewData = placePagePreviewData
self.objectType = objectType
self.placePageData = placePageData
self.placePagePreviewData = placePageData.previewData
self.objectType = placePageData.objectType
self.headerType = headerType
}
}
extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol {
func configure() {
view?.setTitle(placePagePreviewData.title, secondaryTitle: placePagePreviewData.secondaryTitle)
let existenceConfirmation = getExistenceConfirmationText()
view?.setTitle(placePagePreviewData.title, secondaryTitle: placePagePreviewData.secondaryTitle, existenceConfirmation: existenceConfirmation)
switch headerType {
case .flexible:
view?.isExpandViewHidden = false
@@ -68,4 +70,10 @@ extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol {
func onExportTrackButtonPress(_ type: KmlFileType, from sourceView: UIView) {
delegate?.previewDidPressExportTrack(type, from: sourceView)
}
private func getExistenceConfirmationText() -> String? {
guard let mostRecentDate = placePageData.infoData?.getMostRecentCheckDate() else { return nil }
let timeAgoText = mostRecentDate.formatTimeAgo()
return String(format: L("existence_confirmed_time_ago"), timeAgoText)
}
}

View File

@@ -3,7 +3,7 @@ protocol PlacePageHeaderViewProtocol: AnyObject {
var isExpandViewHidden: Bool { get set }
var isShadowViewHidden: Bool { get set }
func setTitle(_ title: String?, secondaryTitle: String?)
func setTitle(_ title: String?, secondaryTitle: String?, existenceConfirmation: String?)
func showShareTrackMenu()
}
@@ -77,7 +77,7 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol {
}
}
func setTitle(_ title: String?, secondaryTitle: String?) {
func setTitle(_ title: String?, secondaryTitle: String?, existenceConfirmation: String? = nil) {
titleText = title
secondaryText = secondaryTitle
// XCode 13 is not smart enough to detect that title is used below, and requires explicit unwrapped variable.
@@ -93,6 +93,18 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol {
let attributedText = NSMutableAttributedString(string: unwrappedTitle, attributes: titleAttributes)
// Add existence confirmation if available
if let existenceText = existenceConfirmation {
let existenceParagraphStyle = NSMutableParagraphStyle()
existenceParagraphStyle.paragraphSpacingBefore = 1
let existenceAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 11),
.foregroundColor: UIColor.blackSecondaryText(),
.paragraphStyle: existenceParagraphStyle
]
attributedText.append(NSAttributedString(string: "\n" + existenceText, attributes: existenceAttributes))
}
guard let unwrappedSecondaryTitle = secondaryTitle else {
titleLabel?.attributedText = attributedText
return

View File

@@ -40,6 +40,8 @@ final class PlacePagePreviewViewController: UIViewController {
}
}
var placePageData: PlacePageData?
private var distance: String? = nil
private var speedAndAltitude: String? = nil
private var heading: CGFloat? = nil
@@ -84,6 +86,9 @@ final class PlacePagePreviewViewController: UIViewController {
subtitleString.append(NSAttributedString(string: !subtitleString.string.isEmpty ? "" + subtitle : subtitle,
attributes: [.foregroundColor : UIColor.blackSecondaryText(),
.font : UIFont.regular14()]))
}
if !subtitleString.string.isEmpty {
subtitleLabel.attributedText = subtitleString
subtitleContainerView.isHidden = false
} else {
@@ -254,6 +259,16 @@ final class PlacePagePreviewViewController: UIViewController {
NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()])
attributedString.append(detailsString)
}
if let openingHoursDate = placePageData?.infoData?.checkDateOpeningHours {
let timeAgoText = openingHoursDate.formatTimeAgo()
let openingHoursDateString = NSAttributedString(string: "" + String(format: L("hours_confirmed_time_ago"), timeAgoText),
attributes: [NSAttributedString.Key.font: UIFont.regular12(),
NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()])
attributedString.append(openingHoursDateString)
}
scheduleLabel.attributedText = attributedString
}
}

View File

@@ -33,6 +33,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
private lazy var previewViewController: PlacePagePreviewViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self)
vc.placePagePreviewData = placePageData.previewData
vc.placePageData = placePageData
return vc
}()
@@ -110,6 +111,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
guard let self = self else { return }
self.actionBarViewController.updateBookmarkButtonState(isSelected: self.placePageData.bookmarkData != nil)
self.previewViewController.placePagePreviewData = self.placePageData.previewData
self.previewViewController.placePageData = self.placePageData
self.updateBookmarkRelatedSections()
}

View File

@@ -28,6 +28,7 @@ class PlacePageTrackLayout: IPlacePageLayout {
private lazy var previewViewController: PlacePagePreviewViewController = {
let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self)
vc.placePagePreviewData = placePageData.previewData
vc.placePageData = placePageData
return vc
}()
@@ -80,6 +81,7 @@ class PlacePageTrackLayout: IPlacePageLayout {
placePageData.onBookmarkStatusUpdate = { [weak self] in
guard let self = self else { return }
self.previewViewController.placePagePreviewData = self.placePageData.previewData
self.previewViewController.placePageData = self.placePageData
self.updateTrackRelatedSections()
}
@@ -113,6 +115,7 @@ private extension PlacePageTrackLayout {
}
if let previewViewController = headerViewControllers.compactMap({ $0 as? PlacePagePreviewViewController }).first {
previewViewController.placePagePreviewData = previewData
previewViewController.placePageData = self.placePageData
previewViewController.updateViews()
}
presenter?.layoutIfNeeded()