protocol PlacePageInteractorProtocol: AnyObject { func viewWillAppear() func viewWillDisappear() func updateTopBound(_ bound: CGFloat, duration: TimeInterval) } class PlacePageInteractor: NSObject { var presenter: PlacePagePresenterProtocol? weak var viewController: UIViewController? weak var mapViewController: MapViewController? weak var trackActivePointPresenter: TrackActivePointPresenter? private let bookmarksManager = BookmarksManager.shared() private var placePageData: PlacePageData private var viewWillAppearIsCalledForTheFirstTime = false init(viewController: UIViewController, data: PlacePageData, mapViewController: MapViewController) { self.placePageData = data self.viewController = viewController self.mapViewController = mapViewController super.init() addToBookmarksManagerObserverList() subscribeOnTrackActivePointUpdatesIfNeeded() } deinit { removeFromBookmarksManagerObserverList() } private func updatePlacePageIfNeeded() { func updatePlacePage() { FrameworkHelper.updatePlacePageData() placePageData.updateBookmarkStatus() } switch placePageData.objectType { case .POI, .trackRecording: break case .bookmark: guard let bookmarkData = placePageData.bookmarkData, bookmarksManager.hasBookmark(bookmarkData.bookmarkId) else { presenter?.closeAnimated() return } updatePlacePage() case .track: guard let trackData = placePageData.trackData, bookmarksManager.hasTrack(trackData.trackId) else { presenter?.closeAnimated() return } updatePlacePage() @unknown default: fatalError("Unknown object type") } } private func subscribeOnTrackActivePointUpdatesIfNeeded() { unsubscribeFromTrackActivePointUpdates() guard placePageData.objectType == .track, let trackData = placePageData.trackData else { return } bookmarksManager.setElevationActivePointChanged(trackData.trackId) { [weak self] distance in self?.trackActivePointPresenter?.updateActivePointDistance(distance) trackData.updateActivePointDistance(distance) } bookmarksManager.setElevationMyPositionChanged(trackData.trackId) { [weak self] distance in self?.trackActivePointPresenter?.updateMyPositionDistance(distance) } } private func unsubscribeFromTrackActivePointUpdates() { bookmarksManager.resetElevationActivePointChanged() bookmarksManager.resetElevationMyPositionChanged() } private func addToBookmarksManagerObserverList() { bookmarksManager.add(self) } private func removeFromBookmarksManagerObserverList() { bookmarksManager.remove(self) } } extension PlacePageInteractor: PlacePageInteractorProtocol { func viewWillAppear() { // Skip data reloading on the first appearance, to avoid unnecessary updates. guard viewWillAppearIsCalledForTheFirstTime else { viewWillAppearIsCalledForTheFirstTime = true return } updatePlacePageIfNeeded() } func viewWillDisappear() { unsubscribeFromTrackActivePointUpdates() } func updateTopBound(_ bound: CGFloat, duration: TimeInterval) { mapViewController?.setPlacePageTopBound(bound, duration: duration) } } // MARK: - PlacePageInfoViewControllerDelegate extension PlacePageInteractor: PlacePageInfoViewControllerDelegate { var shouldShowOpenInApp: Bool { !OpenInApplication.availableApps.isEmpty } func didPressCall(to phone: PlacePagePhone) { MWMPlacePageManagerHelper.call(phone) } func didPressWebsite() { MWMPlacePageManagerHelper.openWebsite(placePageData) } func didPressWebsiteMenu() { MWMPlacePageManagerHelper.openWebsiteMenu(placePageData) } func didPressWikipedia() { MWMPlacePageManagerHelper.openWikipedia(placePageData) } func didPressWikimediaCommons() { MWMPlacePageManagerHelper.openWikimediaCommons(placePageData) } func didPressFediverse() { MWMPlacePageManagerHelper.openFediverse(placePageData) } func didPressFacebook() { MWMPlacePageManagerHelper.openFacebook(placePageData) } func didPressInstagram() { MWMPlacePageManagerHelper.openInstagram(placePageData) } func didPressTwitter() { MWMPlacePageManagerHelper.openTwitter(placePageData) } func didPressVk() { MWMPlacePageManagerHelper.openVk(placePageData) } func didPressLine() { MWMPlacePageManagerHelper.openLine(placePageData) } func didPressBluesky() { MWMPlacePageManagerHelper.openBluesky(placePageData) } func didPressPanoramax() { MWMPlacePageManagerHelper.openPanoramax(placePageData) } func didPressEmail() { MWMPlacePageManagerHelper.openEmail(placePageData) } func didCopy(_ content: String) { UIPasteboard.general.string = content let message = String(format: L("copied_to_clipboard"), content) UIImpactFeedbackGenerator(style: .medium).impactOccurred() Toast.show(withText: message, alignment: .bottom) } func didPressOpenInApp(from sourceView: UIView) { let availableApps = OpenInApplication.availableApps guard !availableApps.isEmpty else { LOG(.warning, "Applications selection sheet should not be presented when the list of available applications is empty.") return } let openInAppActionSheet = UIAlertController.presentInAppActionSheet(from: sourceView, apps: availableApps) { [weak self] selectedApp in guard let self else { return } let link = selectedApp.linkWith(coordinates: self.placePageData.locationCoordinate, destinationName: self.placePageData.previewData.title) self.mapViewController?.openUrl(link, externally: true) } presenter?.showAlert(openInAppActionSheet) } } // MARK: - WikiDescriptionViewControllerDelegate extension PlacePageInteractor: WikiDescriptionViewControllerDelegate { func didPressMore() { MWMPlacePageManagerHelper.showPlaceDescription(placePageData.wikiDescriptionHtml) } } // MARK: - PlacePageButtonsViewControllerDelegate extension PlacePageInteractor: PlacePageButtonsViewControllerDelegate { func didPressHotels() { MWMPlacePageManagerHelper.openDescriptionUrl(placePageData) } func didPressAddPlace() { MWMPlacePageManagerHelper.addPlace(placePageData.locationCoordinate) } func didPressEditPlace() { MWMPlacePageManagerHelper.editPlace() } func didPressAddBusiness() { MWMPlacePageManagerHelper.addBusiness() } } // MARK: - PlacePageEditBookmarkOrTrackViewControllerDelegate extension PlacePageInteractor: PlacePageEditBookmarkOrTrackViewControllerDelegate { func didUpdate(color: UIColor, category: MWMMarkGroupID, for data: PlacePageEditData) { switch data { case .bookmark(let bookmarkData): let bookmarkColor = BookmarkColor.bookmarkColor(from: color) ?? bookmarkData.color MWMPlacePageManagerHelper.updateBookmark(placePageData, color: bookmarkColor, category: category) case .track: MWMPlacePageManagerHelper.updateTrack(placePageData, color: color, category: category) } } func didPressEdit(_ data: PlacePageEditData) { switch data { case .bookmark: MWMPlacePageManagerHelper.editBookmark(placePageData) case .track: MWMPlacePageManagerHelper.editTrack(placePageData) } } } // MARK: - ActionBarViewControllerDelegate extension PlacePageInteractor: ActionBarViewControllerDelegate { func actionBar(_ actionBar: ActionBarViewController, didPressButton type: ActionBarButtonType) { switch type { case .booking: MWMPlacePageManagerHelper.book(placePageData) case .bookingSearch: MWMPlacePageManagerHelper.searchBookingHotels(placePageData) case .bookmark: if placePageData.bookmarkData != nil { MWMPlacePageManagerHelper.removeBookmark(placePageData) } else { MWMPlacePageManagerHelper.addBookmark(placePageData) } case .call: // 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: guard let mapNodeAttributes = placePageData.mapNodeAttributes else { fatalError("Download button can't be displayed if mapNodeAttributes is empty") } switch mapNodeAttributes.nodeStatus { case .downloading, .inQueue, .applying: Storage.shared().cancelDownloadNode(mapNodeAttributes.countryId) case .notDownloaded, .partly, .error: Storage.shared().downloadNode(mapNodeAttributes.countryId) case .undefined, .onDiskOutOfDate, .onDisk: fatalError("Download button shouldn't be displayed when node is in these states") @unknown default: fatalError() } case .opentable: fatalError("Opentable is not supported and will be deleted") case .routeAddStop: MWMPlacePageManagerHelper.routeAddStop(placePageData) case .routeFrom: MWMPlacePageManagerHelper.route(from: placePageData) case .routeRemoveStop: MWMPlacePageManagerHelper.routeRemoveStop(placePageData) case .routeTo: MWMPlacePageManagerHelper.route(to: placePageData) case .avoidToll: MWMPlacePageManagerHelper.avoidToll() case .avoidDirty: MWMPlacePageManagerHelper.avoidDirty() case .avoidFerry: MWMPlacePageManagerHelper.avoidFerry() case .more: fatalError("More button should've been handled in ActionBarViewContoller") case .track: guard placePageData.trackData != nil else { return } // TODO: (KK) This is temporary solution. Remove the dialog and use the MWMPlacePageManagerHelper.removeTrack // directly here when the track recovery mechanism will be implemented. showTrackDeletionConfirmationDialog() case .saveTrackRecording: // TODO: (KK) pass name typed by user TrackRecordingManager.shared.stopAndSave() { [weak self] result in switch result { case .success: break case .trackIsEmpty: self?.presenter?.closeAnimated() } } case .notSaveTrackRecording: TrackRecordingManager.shared.stop() { [weak self] result in self?.presenter?.closeAnimated() } @unknown default: fatalError() } } private func showTrackDeletionConfirmationDialog() { let alert = UIAlertController(title: nil, message: L("placepage_delete_track_confirmation_alert_message"), preferredStyle: .actionSheet) let deleteAction = UIAlertAction(title: L("delete"), style: .destructive) { [weak self] _ in guard let self = self else { return } guard self.placePageData.trackData != nil else { fatalError("The track data should not be nil during the track deletion") } MWMPlacePageManagerHelper.removeTrack(self.placePageData) self.presenter?.closeAnimated() } let cancelAction = UIAlertAction(title: L("cancel"), style: .cancel) alert.addAction(deleteAction) alert.addAction(cancelAction) guard let viewController else { return } iPadSpecific { alert.popoverPresentationController?.sourceView = viewController.view alert.popoverPresentationController?.sourceRect = viewController.view.frame } 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 extension PlacePageInteractor: ElevationProfileViewControllerDelegate { func openDifficultyPopup() { MWMPlacePageManagerHelper.openElevationDifficultPopup(placePageData) } func updateMapPoint(_ point: CLLocationCoordinate2D, distance: Double) { guard let trackData = placePageData.trackData, trackData.elevationProfileData?.isTrackRecording == false else { return } bookmarksManager.setElevationActivePoint(point, distance: distance, trackId: trackData.trackId) placePageData.trackData?.updateActivePointDistance(distance) } } // MARK: - PlacePageHeaderViewController extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate { func previewDidPressClose() { presenter?.closeAnimated() } func previewDidPressExpand() { presenter?.showNextStop() } func previewDidPressShare(from sourceView: UIView) { guard let mapViewController else { return } switch placePageData.objectType { case .POI, .bookmark: let shareViewController = ActivityViewController.share(forPlacePage: placePageData) shareViewController.present(inParentViewController: mapViewController, anchorView: sourceView) case .track: presenter?.showShareTrackMenu() default: guard let coordinates = LocationManager.lastLocation()?.coordinate else { viewController?.present(UIAlertController.unknownCurrentPosition(), animated: true, completion: nil) return } let activity = ActivityViewController.share(forMyPosition: coordinates) activity.present(inParentViewController: mapViewController, anchorView: sourceView) } } func previewDidPressExportTrack(_ type: KmlFileType, from sourceView: UIView) { guard let trackId = placePageData.trackData?.trackId else { fatalError("Track data should not be nil during the track export") } bookmarksManager.shareTrack(trackId, fileType: type) { [weak self] status, url in guard let self, let mapViewController else { return } switch status { case .success: guard let url else { fatalError("Invalid sharing url") } let shareViewController = ActivityViewController.share(for: url, message: self.placePageData.previewData.title!) { _,_,_,_ in self.bookmarksManager.finishSharing() } shareViewController.present(inParentViewController: mapViewController, anchorView: sourceView) case .emptyCategory: self.showAlert(withTitle: L("bookmarks_error_title_share_empty"), message: L("bookmarks_error_message_share_empty")) case .archiveError, .fileError: self.showAlert(withTitle: L("dialog_routing_system_error"), message: L("bookmarks_error_message_share_general")) } } } private func showAlert(withTitle title: String, message: String) { MWMAlertViewController.activeAlert().presentInfoAlert(title, text: message) } } // MARK: - BookmarksObserver extension PlacePageInteractor: BookmarksObserver { func onBookmarksLoadFinished() { updatePlacePageIfNeeded() } func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) { guard let bookmarkGroupId = placePageData.bookmarkData?.bookmarkGroupId else { return } if bookmarkGroupId == groupId { presenter?.closeAnimated() } } }