diff --git a/README.md b/README.md index 280d58b..b7c3e21 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,257 @@ -## MAPS.ME iOS API: Getting Started +## Organic Maps iOS API: Getting Started ### Introduction -MAPS.ME (MapsWithMe) offline maps API for iOS (hereinafter referred to as *API*) provides an interface for other applications to perform the following tasks: +Organic Maps offline maps API for iOS (hereinafter referred to as *API*) provides an interface for other applications to perform the following tasks (even while in offline): -For API version 1 (supported by MapsWithMe 2.4+) -* Open [MapsWithMe Application][linkMwm] -* Check that [MapsWithMe][linkMwm] is installed -* Show one or more points on an offline map of [MapsWithMe][linkMwm] with *Back* button and client app name in the title +* Open [Organic Maps Application][linkOm] +* Check that [Organic Maps][linkOm] is installed +* Show one or more points on an offline map of [Organic Maps][linkOm] with *Back* button and client app name in the title * Return the user back to the client application: * after pressing *Back* button on the map - * after selecting specific point on the map if user asks for more information by pressing *More Info* button in [MapsWithMe][linkMwm] -* Open any given url or url scheme after selecting specific point on the map if user asks for more information by pressing *More Info* button in [MapsWithMe][linkMwm] -* Automatically display [*Download MapsWithMe*][linkDownloadMWMDialog] dialog if [MapsWithMe][linkMwm] is not installed. + * after selecting specific point on the map if user asks for more information by pressing *More Info* button in [Organic Maps][linkOm] +* Open any given url or url scheme after selecting specific point on the map if user asks for more information by pressing *More Info* button in [Organic Maps][linkOm] +* Automatically display *Download Organic Maps* dialog if [Organic Maps][linkOm] is not installed. -In general it is possible to establish a one way or two way communication between MapsWithMe and your app. - -Please check our [offline travel guide apps][linkTravelGuides] as an API integration example. +In general it is possible to establish a one way or two way communication between *Organic Maps* and your app. ### Prerequisites -* Your application must target at least *iOS version 5.0* -* For two way communication, you should add unique [URL scheme][linkAppleCustomUrlSchemes] to your app (see below) +* Your application must target *iOS version 15.0* or above. +* For two way communication, you should add a unique [URL scheme][linkAppleCustomUrlSchemes] to your app (see below). ### Integration -First step is to clone [repository][linkRepo] or download it as an archive. +First step is to clone the [repository][linkRepo] or download it as an archive. + +The library lives in the `api/` folder and comes in two flavours — pick whichever matches your project: + +* **Objective-C** — copy `api/OrganicMapsAPI.h` and `api/OrganicMapsAPI.m` into your project. Both files are ARC-compatible. +* **Swift 5** — copy `api/OrganicMapsAPI.swift` into your project. + +You can always modify them according to your needs. The two implementations are functionally equivalent and use the same URL scheme on the wire, so apps built with either one interoperate with Organic Maps in the same way. + +If you want to get results of API calls, register a unique URL scheme for your app. You can do it with [XCode][linkAddUrlScheme] or by editing the Info.plist file in your project — see Apple's [documentation][linkAppleCustomUrlSchemes]. The API library automatically picks up the first scheme listed under `CFBundleURLTypes` → `CFBundleURLSchemes` and passes it to Organic Maps as the callback. If you register multiple URL types, place the one you want Organic Maps to use for callbacks first. -When your are done you find two folders: *api* and *capitals-example*. -First one contains .h and .m files which you need to include into your project. You can always modify them according to your needs. +Organic Maps uses the `om://` URL scheme. -If you want to get results of API calls, please add unique URL scheme to your app. You can do it with [XCode][linkAddUrlScheme] or by editing Info.plist file in your project. To make things simple, use *mapswithme* keyword in scheme ID, like *my_mapswithme_scheme*, and create an unique scheme name (or use your existing one). -*mapswithme* keyword in scheme ID simply helps API code to detect it automatically. See more details in [Apple's documentation][linkAppleCustomUrlSchemes]. +You also need to add [LSApplicationQueriesSchemes][linkAppleLSApplicationQueriesSchemes] key into your plist with value *om* to correctly query whether Organic Maps is already installed. -MAPS.ME (MapsWithMe) supports two schemes: "mapswithme://" and "mapswithmepro://" +### Repository layout -iOS9+ note: you need to add LSApplicationQueriesSchemes key into your plist with value mapswithme to correctly query if MAPS.ME is installed. +``` +api-ios/ +├── api/ +│ ├── OrganicMapsAPI.h # Objective-C header +│ ├── OrganicMapsAPI.m # Objective-C implementation +│ └── OrganicMapsAPI.swift # Swift 5 implementation +├── shared-resources/ +│ └── capitals.plist # Sample data shared by both example apps +├── capitals-example/ # Objective-C sample app +└── swift-capitals-example/ # Swift sample app +``` -*capitals-example* folder contains sample application which demonstrates part of API features. +Both example apps reference `shared-resources/capitals.plist` via a relative path in their Xcode projects, so the data file lives in one place. To share other resources across the two examples (icons, plists, etc.), add them under `shared-resources/` and reference them from each `.xcodeproj` the same way — no symlinks needed. -*NOTE: If you are using Automatic References Counting (ARC) in your project, you can use [this solution][linkFixARC] or simply fix code by yourself.* +*NOTE: If you are using Automatic References Counting (ARC) in your project, you can use [this solution][linkFixARC] or simply fix the code by yourself.* ### API Calls Overview and HOW TO -* All methods are static for *MWMApi* class, *BOOL* methods return *NO* if call is failed. -* If id for given pin contains valid url, it will be opened from MapsWithMe after selecting *More Info* button. - For any other content, id will be simply passed back to the caller's [*AppDelegate application:openURL:sourceApplication:annotation:*][linkAppleDelegate] method +* Public methods on the Objective-C `OMApi` class are static; `BOOL`-returning methods return `NO` on failure. The Swift equivalent is the `OrganicMaps` enum (used as a namespace) with the same methods. +* If `id` for a given pin contains a valid URL, it will be opened from Organic Maps after the user selects *More Info*. For any other content, the id is passed back to the caller's [`application:openURL:options:`][linkAppleDelegate] method. -#### Open [MapsWithMe Application][linkMwm] +#### Open [Organic Maps Application][linkOm] -Simply opens MapsWithMe app: +Simply opens Organic Maps app: + +Objective-C: + (BOOL)showMap; - -Example: + // … + [OMApi showMap]; + +Swift: - [MWMApi showMap]; + static func openApp() -> Bool + // … + OrganicMaps.openApp() #### Show specified location on the map Displays given point on a map: - + (BOOL)showLat:(double)lat lon:(double)lon title:(NSString *)title and:(NSString *)idOrUrl; +Objective-C: -The same as above but using pin wrapper: + + (BOOL)showLat:(double)lat lon:(double)lon title:(nullable NSString *)title idOrUrl:(nullable NSString *)idOrUrl; + + (BOOL)showPin:(nullable OMPin *)pin; - + (BOOL)showPin:(MWMPin *)pin; + @interface OMPin : NSObject + @property (nonatomic) double lat; + @property (nonatomic) double lon; + @property (nullable, copy, nonatomic) NSString * title; + @property (nullable, copy, nonatomic) NSString * idOrUrl; + - (nullable instancetype)initWithLat:(double)lat lon:(double)lon + title:(nullable NSString *)title + idOrUrl:(nullable NSString *)idOrUrl; + @end -Pin wrapper is a simple helper to wrap pins displayed on the map: + // … + [OMApi showLat:53.9 lon:27.56667 title:@"Minsk - the capital of Belarus" idOrUrl:@"https://wikipedia.org/wiki/Minsk"]; - @interface MWMPin : NSObject - @property (nonatomic, assign) double lat; - @property (nonatomic, assign) double lon; - @property (nonatomic, retain) NSString * title; - @property (nonatomic, retain) NSString * idOrUrl; - - (id)initWithLat:(double)lat lon:(double)lon title:(NSString *)title and:(NSString *)idOrUrl; - @end + OMPin * goldenGate = [[OMPin alloc] initWithLat:37.8195 lon:-122.4785 + title:@"Golden Gate in San Francisco" + idOrUrl:@"any string or URL"]; + [OMApi showPin:goldenGate]; -Example: +Swift: - [MWMApi showLat:53.9 lon:27.56667 title:@"Minsk - the capital of Belarus" and:@"http://wikipedia.org/wiki/Minsk"]; - … - MWMPin * goldenGate = [[MWMPin alloc] init] autorelease]; - goldenGate.lat = 37.8195; - goldenGate.lon = -122.4785; - goldenGate.title = @"Golden Gate in San Francisco"; - goldenGate.idOrUrl = @"any number or string here you want to receive back in your app, or any url you want to be opened from MapsWithMe"; - [MWMApi showPin:goldenGate]; + @discardableResult + static func showPin(latitude: Double, longitude: Double, + title: String? = nil, identifier: String? = nil) -> Bool + + struct OMPin { + let latitude: Double + let longitude: Double + let title: String? + let identifier: String? + } + + // … + OrganicMaps.showPin(latitude: 53.9, longitude: 27.56667, + title: "Minsk - the capital of Belarus", + identifier: "https://wikipedia.org/wiki/Minsk") #### Show any number of pins on the map - + (BOOL)showPins:(NSArray *)pins; +Objective-C: + + + (BOOL)showPins:(NSArray *)pins; + +Swift: + + @discardableResult + static func showPins(_ pins: [OMPin], openUrlOnBalloonClick: Bool = false) -> Bool #### Receiving results of API calls -When users presses *Back* button in MapsWithMe, or selects *More Info* button, he is redirected back to your app. -Here are helper methods to obtain API call results: +When the user presses *Back* in Organic Maps or selects *More Info*, they are redirected back to your app. Helper methods to obtain the API call result: + +Returns true if `url` is the Organic Maps callback (its scheme matches your app's first registered URL scheme): + +Objective-C: + + + (BOOL)isOrganicMapsUrl:(NSURL *)url; -Returns YES if url is received from MapsWithMe and can be parsed: +Swift: - + (BOOL)isMapsWithMeUrl:(NSURL *)url; + static func isOrganicMapsCallback(url: URL) -> Bool -Returns nil if user didn't select any pin and simply pressed *Back* button: +Returns nil if the user pressed *Back* without selecting any pin: - + (MWMPin *)pinFromUrl:(NSURL *)url; +Objective-C: -Example: + + (nullable OMPin *)pinFromUrl:(NSURL *)url; - if ([MWMApi isMapsWithMeUrl:url]) +Swift: + + static func pin(from url: URL) -> OMPin? + +Example handler in `application:openURL:options:`: + +Objective-C: + + - (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { - // Good, here we know that your app was opened from MapsWithMe - MWMPin * pin = [MWMApi pinFromUrl:url]; - if (pin) + if ([OMApi isOrganicMapsUrl:url]) { - // User selected specific pin, and we can get it's properties + OMPin * pin = [OMApi pinFromUrl:url]; + if (pin) { + // User selected a specific pin; pin.title / pin.idOrUrl / pin.lat / pin.lon are available. + } else { + // User pressed "Back" without selecting any pin. + } + return YES; } - else - { - // User pressed "Back" button and didn't select any pin + return NO; + } + +Swift: + + func application(_ app: UIApplication, open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + guard OrganicMaps.isOrganicMapsCallback(url: url) else { return false } + if let pin = OrganicMaps.pin(from: url) { + // pin.title / pin.identifier / pin.latitude / pin.longitude + } else { + // User pressed "Back" without selecting any pin. } + return true } -Note, that you can simply check that *sourceApplication* contains *com.mapswithme.* substring to detect that your app is opened from MapsWithMe. +Note, that you can simply check that `options[UIApplicationOpenURLOptionsSourceApplicationKey]` contains *app.organicmaps* substring to detect that your app was opened from Organic Maps. + +#### Check that Organic Maps is installed -#### Check that MapsWithMe is installed +Returns false if Organic Maps is not installed or doesn't support API calls: -Returns NO if MapsWithMe is not installed or outdated version doesn't support API calls: +Objective-C: + (BOOL)isApiSupported; -With this method you can check that user needs to install MapsWithMe and display your custom UI. -Alternatively, you can do nothing and use built-in dialog which will offer users to install MapsWithMe. +Swift: -### Set value if you want to open pin URL on balloon click (Available in 2.4.5) + static var isInstalled: Bool { get } + +With this method you can check whether the user needs to install Organic Maps and display your custom UI. Alternatively, the library presents a built-in install dialog (`showOrganicMapsNotInstalledDialog` / `OrganicMaps.presentInstallDialog()`) which is invoked automatically by `showPins:` / `showPins(_:)` when the app isn't installed. + +### Set value if you want to open pin URL on balloon click + +Objective-C: + (void)setOpenUrlOnBalloonClick:(BOOL)value; +Swift (passed per call): + + OrganicMaps.showPins(pins, openUrlOnBalloonClick: true) + ### Under the hood -If you prefer to use API on your own, here are some details about the implementation. +If you prefer to use API directly, here are some details about the implementation. -Applications "talk" to each other using URL Scheme. API v1 supports the following parameters to the URL Scheme: +Applications "talk" to each other using URL Scheme. API v1 supports the following parameters in the URL Scheme: - mapswithme://map?v=1&ll=54.32123,12.34562&n=Point%20Name&id=AnyStringOrEncodedUrl&backurl=UrlToCallOnBackButton&appname=TitleToDisplayInNavBar + om://map?v=1&ll=54.32123,12.34562&n=Point%20Name&id=AnyStringOrEncodedUrl&backurl=UrlToCallOnBackButton&appname=TitleToDisplayInNavBar * **v** - API version, currently *1* * **ll** - pin latitude and longitude, comma-separated * **n** - pin title * **id** - any string you want to receive back in your app, OR alternatively, any valid URL which will be opened on *More Info* button click * **backurl** - usually, your unique app scheme to open back your app -* **appname** - string to display in navigation bar on top of the map in MAPS.ME -* **balloonAction** - pass openUrlOnBalloonClick as a parameter, if you want to open pin url on balloon click(Usually pin url opens when "Show more info" button is pressed). (Available in 2.4.5) +* **appname** - string to display in navigation bar on top of the map in Organic Maps +* **balloonaction** - pass openUrlOnBalloonClick as a parameter, if you want to open pin url on balloon click (usually pin url opens when "Show more info" button is pressed). -Note that you can display as many pins as you want, the only rule is that **ll** parameter comes before **n** and **id** for each point. +Note that you can display as many pins as you want, the only rule is that **ll** parameter comes before **n** and **id** for each point. When user selects a pin, your app is called like this: YourAppUniqueUrlScheme://pin?ll=lat,lon&n=PinName&id=PinId +Organic Maps also supports v2 route deep links. You can open them directly from +your app, for example to preview a route with intermediate stops: + + NSURL * url = [NSURL URLWithString:@"om://v2/dir?origin=52.5200,13.4050&origin_name=Warehouse%20Berlin&destination=52.5163,13.3777&destination_name=Customer&waypoints=52.5304,13.3850|52.5450,13.3920&waypoint_names=Pickup%201|Pickup%202&mode=drive"]; + [[UIApplication sharedApplication] openURL:url]; + +Use `om://v2/nav` instead of `om://v2/dir` to start navigation when the route is +ready. If `origin` is an explicit coordinate instead of `currentLocation`, +Organic Maps previews the route first so the user can confirm the start point. + ------------------------------------------------------------------------------------------ ### API Code is licensed under the BSD 2-Clause License +Copyright (c) 2022, Organic Maps OÜ Copyright (c) 2019, MY.COM B.V. All rights reserved. @@ -171,12 +262,11 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -[linkMwm]: https://maps.me/ "MAPS.ME - offline maps of the world" -[linkRepo]: https://github.com/mapsme/api-ios "GitHub Repository" -[linkAddUrlScheme]: https://raw.github.com/mapswithme/api-ios/site-resources/add_custom_url_scheme.png "How to add url scheme in XCode" -[linkDownloadMWMDialog]: https://raw.github.com/mapswithme/api-ios/site-resources/download_mwm_dialog.png "Donwload MAPS.ME Dialog" -[linkIssues]: https://github.com/mapsme/api-ios/issues "Post a bug or feature request" +[linkOm]: https://organicmaps.app/ "Organic Maps: free, privacy-focused, fast and detailed offline maps app" +[linkRepo]: https://github.com/organicmaps/api-ios "GitHub Repository" +[linkAddUrlScheme]: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app "How to add url scheme in XCode" +[linkIssues]: https://github.com/organicmaps/api-ios/issues "Post a bug or feature request" [linkAppleCustomUrlSchemes]: https://developer.apple.com/library/ios/#DOCUMENTATION/iPhone/Conceptual/iPhoneOSProgrammingGuide/AdvancedAppTricks/AdvancedAppTricks.html#//apple_ref/doc/uid/TP40007072-CH7-SW50 "Custom URL Scheme Apple documentation" -[linkAppleDelegate]: https://developer.apple.com/library/ios/documentation/uikit/reference/UIApplicationDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIApplicationDelegate/application:openURL:sourceApplication:annotation: "AppDelegate Handle custom URL Schemes" +[linkAppleDelegate]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:options:) "AppDelegate Handle custom URL Schemes" [linkFixARC]: http://stackoverflow.com/a/6658549/1209392 "How to compile non-ARC code in ARC projects" -[linkTravelGuides]: http://guidewithme.com "Offline Travel Guides" +[linkAppleLSApplicationQueriesSchemes]: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW14 "LSApplicationQueriesSchemes" diff --git a/api/MapsWithMeAPI.m b/api/MapsWithMeAPI.m deleted file mode 100644 index 371aac9..0000000 --- a/api/MapsWithMeAPI.m +++ /dev/null @@ -1,262 +0,0 @@ -/******************************************************************************* - - Copyright (c) 2014, MapsWithMe GmbH - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ******************************************************************************/ - -#import "MapsWithMeAPI.h" - -#define MAPSWITHME_API_VERSION 1.1 - -static NSString * const kMWMUrlScheme = @"mapswithme://"; -static BOOL kOpenUrlOnBalloonClick = NO; - -@implementation MWMPin - -- (nullable instancetype)init -{ - self = [super init]; - if (self) - { - _lat = INFINITY; - _lon = INFINITY; - } - return self; -} - -- (nullable instancetype)initWithLat:(double)lat - lon:(double)lon - title:(nullable NSString *)title - idOrUrl:(nullable NSString *)idOrUrl -{ - self = [super init]; - if (self) - { - _lat = lat; - _lon = lon; - _title = title; - _idOrUrl = idOrUrl; - } - return self; -} - -@end - -// Utility class to automatically handle "MapsWithMe is not installed" situations -@interface MWMNViewController : UIViewController - -@end - -@implementation MWMNViewController - -// HTML page for users who didn't install MapsWithMe -static NSString * const mapsWithMeIsNotInstalledPage = -@"" \ -"" \ -"Please install MAPS.ME - offline maps of the World" \ -"" \ -"" \ -"" \ -"" \ -"" \ -"
Offline maps are required to proceed. We have partnered with MAPS.ME to provide you with offline maps of the entire world.
" \ -"
To continue please download the app:
" \ -"Download MAPS.ME" \ -"" \ -""; - -- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error -{ - [(UIWebView *)self.view loadHTMLString:mapsWithMeIsNotInstalledPage baseURL:[NSURL URLWithString:@"http://maps.me/"]]; -} - -- (void)onCloseButtonClicked:(id)sender -{ - [self dismissViewControllerAnimated:YES completion:nil]; -} - -@end - - -@implementation MWMApi - -+ (NSString *)urlEncode:(NSString *)str -{ - return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR("!$&'()*+,-./:;=?@_~"), kCFStringEncodingUTF8); -} - -+ (BOOL)isMapsWithMeUrl:(nonnull NSURL *)url -{ - NSString * appScheme = [MWMApi detectBackUrlScheme]; - return appScheme && [url.scheme isEqualToString:appScheme]; -} - -+ (nullable MWMPin *)pinFromUrl:(nonnull NSURL *)url -{ - if (![MWMApi isMapsWithMeUrl:url]) - return nil; - - MWMPin * pin = nil; - if ([url.host isEqualToString:@"pin"]) - { - pin = [[MWMPin alloc] init]; - for (NSString * param in [url.query componentsSeparatedByString:@"&"]) - { - NSArray * values = [param componentsSeparatedByString:@"="]; - if ([values count] == 2) - { - NSString * key = values[0]; - if ([key isEqualToString:@"ll"]) - { - NSArray * coords = [values[1] componentsSeparatedByString:@","]; - if ([coords count] == 2) - { - pin.lat = [[NSDecimalNumber decimalNumberWithString:coords[0]] doubleValue]; - pin.lon = [[NSDecimalNumber decimalNumberWithString:coords[1]] doubleValue]; - } - } - else if ([key isEqualToString:@"n"]) - pin.title = [values[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - else if ([key isEqualToString:@"id"]) - pin.idOrUrl = [values[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - else - NSLog(@"Unsupported url parameters: %@", values); - } - } - // do not accept invalid coordinates - if (pin.lat > 90. || pin.lat < -90. || pin.lon > 180. || pin.lon < -180.) - pin = nil; - } - return pin; -} - -+ (BOOL)isApiSupported -{ - return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:kMWMUrlScheme]]; -} - -+ (BOOL)showMap -{ - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[kMWMUrlScheme stringByAppendingFormat:@"map?v=%f", MAPSWITHME_API_VERSION]]]; -} - -+ (BOOL)showLat:(double)lat lon:(double)lon title:(nullable NSString *)title idOrUrl:(nullable NSString *)idOrUrl -{ - MWMPin * pin = [[MWMPin alloc] initWithLat:lat lon:lon title:title idOrUrl:idOrUrl]; - return [MWMApi showPin:pin]; -} - -+ (BOOL)showPin:(nullable MWMPin *)pin -{ - return pin ? [MWMApi showPins:@[pin]] : NO; -} - -+ (BOOL)showPins:(nonnull NSArray *)pins -{ - // Automatic check that MapsWithMe is installed - if (![MWMApi isApiSupported]) - { - // Display dialog with link to the app - [MWMApi showMapsWithMeIsNotInstalledDialog]; - return NO; - } - - NSString * appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; - NSMutableString * str = [NSMutableString stringWithFormat:@"%@map?v=%f&appname=%@&", - kMWMUrlScheme, - MAPSWITHME_API_VERSION, - [self urlEncode:appName]]; - - NSString * backUrlScheme = [MWMApi detectBackUrlScheme]; - - if (backUrlScheme) - [str appendFormat:@"backurl=%@&", [self urlEncode:backUrlScheme]]; - - for (MWMPin * point in pins) - { - [str appendFormat:@"ll=%f,%f&", point.lat, point.lon]; - @autoreleasepool - { - if (point.title) - [str appendFormat:@"n=%@&", [self urlEncode:point.title]]; - if (point.idOrUrl) - [str appendFormat:@"id=%@&", [self urlEncode:point.idOrUrl]]; - } - } - - if (kOpenUrlOnBalloonClick) - [str appendString:@"&balloonAction=kOpenUrlOnBalloonClick"]; - - NSURL * url = [NSURL URLWithString:str]; - BOOL const result = [[UIApplication sharedApplication] openURL:url]; - return result; -} - -+ (NSString *)detectBackUrlScheme -{ - for (NSDictionary * dict in [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]) - { - if ([dict[@"CFBundleURLName"] rangeOfString:@"mapswithme" options:NSCaseInsensitiveSearch].location != NSNotFound) - { - for (NSString * scheme in dict[@"CFBundleURLSchemes"]) - { - // We use the first scheme in this list, you can change this behavior if needed - return scheme; - } - } - } - NSLog(@"WARNING: No com.mapswithme.maps url schemes are added in the Info.plist file. Please add them if you want API users to come back to your app."); - return nil; -} - -+ (void)showMapsWithMeIsNotInstalledDialog -{ - UIWebView * webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame]; - // check that we have Internet connection and display fresh online page if possible - [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://maps.me/api_mwm_not_installed"]]]; - MWMNViewController * webController = [[MWMNViewController alloc] init]; - webView.delegate = webController; - webController.view = webView; - webController.title = @"Install MAPS.ME"; - UINavigationController * navController = [[UINavigationController alloc] initWithRootViewController:webController]; - navController.navigationBar.topItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStyleDone target:webController action:@selector(onCloseButtonClicked:)]; - - UIWindow * window = [[UIApplication sharedApplication].windows firstObject]; - [window.rootViewController presentViewController:navController animated:YES completion:nil]; -} - -+ (void)setOpenUrlOnBalloonClick:(BOOL)value -{ - kOpenUrlOnBalloonClick = value; -} - -@end diff --git a/api/MapsWithMeAPI.h b/api/OrganicMapsAPI.h similarity index 75% rename from api/MapsWithMeAPI.h rename to api/OrganicMapsAPI.h index 10c830b..0450ae1 100644 --- a/api/MapsWithMeAPI.h +++ b/api/OrganicMapsAPI.h @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2014, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -28,12 +28,12 @@ #import -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0 - #error "maps.me supports iOS >= 7.0 only" +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 + #error "Organic Maps supports iOS >= 15.0 only" #endif // Wrapper for a pin on a map -@interface MWMPin : NSObject +@interface OMPin : NSObject /// [required] pin latitude @property (nonatomic) double lat; /// [required] pin longitude @@ -41,7 +41,7 @@ /// [optional] pin title @property (nullable, copy, nonatomic) NSString * title; /// [optional] passed back to the app when pin is clicked, OR, if it's a valid url, -/// it will be opened from MapsWithMe after selecting "More Details..." for the pin +/// it will be opened from Organic Maps after selecting "More Details..." for the pin. @property (nullable, copy, nonatomic) NSString * idOrUrl; - (nullable instancetype)initWithLat:(double)lat @@ -52,26 +52,26 @@ @end -// MapsWithMe API interface -@interface MWMApi : NSObject +// Organic Maps API interface +@interface OMApi : NSObject -/// returns YES if url is received from MapsWithMe and can be parsed -+ (BOOL)isMapsWithMeUrl:(nonnull NSURL *)url; +/// returns YES if url is received from Organic Maps and can be parsed ++ (BOOL)isOrganicMapsUrl:(nonnull NSURL *)url; /// returns nil if user didn't select any pin and simply pressed "Back" button -+ (nullable MWMPin *)pinFromUrl:(nonnull NSURL *)url; -/// returns NO if MapsWithMe is not installed or outdated version doesn't support API calls ++ (nullable OMPin *)pinFromUrl:(nonnull NSURL *)url; +/// returns NO if Organic Maps is not installed or outdated version doesn't support API calls + (BOOL)isApiSupported; -/// Simply opens MapsWithMe app +/// Simply opens Organic Maps app + (BOOL)showMap; /// Displays given point on a map, title and id are optional. -/// If id contains valid url, it will be opened from MapsWithMe after selecting "More Details..." for the pin +/// If id contains valid url, it will be opened from Organic Maps after selecting "More Details..." for the pin + (BOOL)showLat:(double)lat lon:(double)lon title:(nullable NSString *)title idOrUrl:(nullable NSString *)idOrUrl; /// The same as above but using pin wrapper -+ (BOOL)showPin:(nullable MWMPin *)pin; ++ (BOOL)showPin:(nullable OMPin *)pin; /// Displays any number of pins -+ (BOOL)showPins:(nonnull NSArray *)pins; ++ (BOOL)showPins:(nonnull NSArray *)pins; // -+ (void)showMapsWithMeIsNotInstalledDialog; ++ (void)showOrganicMapsNotInstalledDialog; /// Set value = YES if you want to open pin URL on balloon click, default value is NO + (void)setOpenUrlOnBalloonClick:(BOOL)value; diff --git a/api/OrganicMapsAPI.m b/api/OrganicMapsAPI.m new file mode 100644 index 0000000..7d14178 --- /dev/null +++ b/api/OrganicMapsAPI.m @@ -0,0 +1,327 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +#import "OrganicMapsAPI.h" + +#import +#import + +#define OM_API_VERSION 1 + +static NSString * const kOMUrlScheme = @"om://"; +static NSString * const kOMNotInstalledPageUrl = @"https://omaps.app/get"; +static BOOL kOpenUrlOnBalloonClick = NO; + +@implementation OMPin + +- (nullable instancetype)init +{ + self = [super init]; + if (self) + { + _lat = INFINITY; + _lon = INFINITY; + } + return self; +} + +- (nullable instancetype)initWithLat:(double)lat + lon:(double)lon + title:(nullable NSString *)title + idOrUrl:(nullable NSString *)idOrUrl +{ + self = [super init]; + if (self) + { + _lat = lat; + _lon = lon; + _title = title; + _idOrUrl = idOrUrl; + } + return self; +} + +@end + +// Embedded fallback HTML shown when omaps.app cannot be reached (e.g. user is offline). +static NSString * const kOrganicMapsIsNotInstalledPage = +@"" +"" +"Please install Organic Maps: free, open-source, detailed and fast offline maps" +"" +"" +"" +"" +"" +"
Organic Maps app is required to proceed. We integrated with Organic Maps to provide you with offline maps of the entire world.
" +"
To continue please download the app:
" +"Download Organic Maps" +"" +""; + +// Private view controller wrapping a WKWebView to display the "install Organic Maps" page. +@interface OMNViewController : UIViewController +@property (nonatomic, strong) WKWebView * webView; +@end + +@implementation OMNViewController + +- (WKWebView *)webView +{ + if (!_webView) + { + _webView = [[WKWebView alloc] init]; + _webView.navigationDelegate = self; + } + return _webView; +} + +- (void)loadView +{ + self.view = self.webView; +} + +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + [webView loadHTMLString:kOrganicMapsIsNotInstalledPage baseURL:nil]; +} + +// Route link taps to Safari / App Store so the user can actually install Organic Maps. +// Without this, WKWebView follows the link in-place and nothing visible happens. +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + if (navigationAction.navigationType == WKNavigationTypeLinkActivated) + { + NSURL * url = navigationAction.request.URL; + if (url) + [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)onCloseButtonClicked:(id)sender +{ + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end + + +@implementation OMApi + ++ (BOOL)isOrganicMapsUrl:(nonnull NSURL *)url +{ + NSString * appScheme = [OMApi detectBackUrlScheme]; + return appScheme && [url.scheme isEqualToString:appScheme]; +} + ++ (nullable OMPin *)pinFromUrl:(nonnull NSURL *)url +{ + if (![OMApi isOrganicMapsUrl:url]) + return nil; + if (![url.host isEqualToString:@"pin"]) + return nil; + + OMPin * pin = [[OMPin alloc] init]; + NSURLComponents * components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem * item in components.queryItems) + { + NSString * key = item.name; + NSString * value = item.value; + if (value == nil) + continue; + + if ([key isEqualToString:@"ll"]) + { + NSArray * coords = [value componentsSeparatedByString:@","]; + if (coords.count == 2) + { + // -doubleValue is locale-independent (always treats '.' as the decimal separator), + // unlike NSDecimalNumber which honours the user's current locale. + pin.lat = coords[0].doubleValue; + pin.lon = coords[1].doubleValue; + } + } + else if ([key isEqualToString:@"n"]) + { + pin.title = value; + } + else if ([key isEqualToString:@"id"]) + { + pin.idOrUrl = value; + } + else + { + NSLog(@"Unsupported url parameter: %@=%@", key, value); + } + } + // Reject invalid coordinates. + if (pin.lat > 90. || pin.lat < -90. || pin.lon > 180. || pin.lon < -180.) + return nil; + return pin; +} + ++ (BOOL)isApiSupported +{ + return [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString:kOMUrlScheme]]; +} + ++ (BOOL)showMap +{ + NSURL * url = [NSURL URLWithString:kOMUrlScheme]; + if (![UIApplication.sharedApplication canOpenURL:url]) + return NO; + [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; + return YES; +} + ++ (BOOL)showLat:(double)lat lon:(double)lon title:(nullable NSString *)title idOrUrl:(nullable NSString *)idOrUrl +{ + OMPin * pin = [[OMPin alloc] initWithLat:lat lon:lon title:title idOrUrl:idOrUrl]; + return [OMApi showPin:pin]; +} + ++ (BOOL)showPin:(nullable OMPin *)pin +{ + return pin ? [OMApi showPins:@[pin]] : NO; +} + ++ (BOOL)showPins:(nonnull NSArray *)pins +{ + if (![OMApi isApiSupported]) + { + [OMApi showOrganicMapsNotInstalledDialog]; + return NO; + } + + NSURLComponents * components = [[NSURLComponents alloc] init]; + components.scheme = @"om"; + components.host = @"map"; + + NSMutableArray * queryItems = [NSMutableArray array]; + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"v" + value:[NSString stringWithFormat:@"%d", OM_API_VERSION]]]; + + NSString * appName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + if (appName.length > 0) + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"appname" value:appName]]; + + NSString * backUrlScheme = [OMApi detectBackUrlScheme]; + if (backUrlScheme.length > 0) + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"backurl" value:backUrlScheme]]; + + for (OMPin * point in pins) + { + // The parser requires `ll` to come before `n` and `id` for each point. + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"ll" + value:[NSString stringWithFormat:@"%f,%f", + point.lat, point.lon]]]; + if (point.title.length > 0) + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"n" value:point.title]]; + if (point.idOrUrl.length > 0) + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"id" value:point.idOrUrl]]; + } + + if (kOpenUrlOnBalloonClick) + [queryItems addObject:[NSURLQueryItem queryItemWithName:@"balloonaction" value:@"openUrlOnBalloonClick"]]; + + components.queryItems = queryItems; + NSURL * url = components.URL; + if (url == nil) + return NO; + + [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; + return YES; +} + +// Returns the first URL scheme registered by the host app in Info.plist +// (CFBundleURLTypes > CFBundleURLSchemes). This scheme is passed to Organic Maps +// as `backurl=` so it can call the host app back when the user finishes interacting +// with the map. Apps with multiple registered URL types should place the scheme they +// want Organic Maps to use first. ++ (NSString *)detectBackUrlScheme +{ + NSArray * urlTypes = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + for (NSDictionary * urlType in urlTypes) + { + NSArray * schemes = urlType[@"CFBundleURLSchemes"]; + for (NSString * scheme in schemes) + return scheme; + } + NSLog(@"WARNING: No URL scheme is registered in CFBundleURLTypes. Add one to allow Organic Maps to return the user to your app."); + return nil; +} + ++ (void)showOrganicMapsNotInstalledDialog +{ + OMNViewController * webController = [[OMNViewController alloc] init]; + webController.title = @"Install Organic Maps"; + [webController.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kOMNotInstalledPageUrl]]]; + + UINavigationController * navController = [[UINavigationController alloc] initWithRootViewController:webController]; + webController.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStyleDone + target:webController + action:@selector(onCloseButtonClicked:)]; + + [[OMApi keyWindow].rootViewController presentViewController:navController animated:YES completion:nil]; +} + ++ (UIWindow *)keyWindow +{ + for (UIScene * scene in UIApplication.sharedApplication.connectedScenes) + { + if (![scene isKindOfClass:UIWindowScene.class]) + continue; + for (UIWindow * window in ((UIWindowScene *)scene).windows) + { + if (window.isKeyWindow) + return window; + } + } + return nil; +} + ++ (void)setOpenUrlOnBalloonClick:(BOOL)value +{ + kOpenUrlOnBalloonClick = value; +} + +@end diff --git a/api/OrganicMapsAPI.swift b/api/OrganicMapsAPI.swift new file mode 100644 index 0000000..4867050 --- /dev/null +++ b/api/OrganicMapsAPI.swift @@ -0,0 +1,241 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +import UIKit +import WebKit + +/// A single point displayed on the Organic Maps map. +public struct OMPin { + public let latitude: Double + public let longitude: Double + /// Optional pin label shown on the map. + public let title: String? + /// Echoed back to the calling app when the pin is selected. If this is a valid URL, + /// Organic Maps opens it on "More Info" instead of returning to the calling app. + public let identifier: String? + + public init(latitude: Double, longitude: Double, title: String? = nil, identifier: String? = nil) { + self.latitude = latitude + self.longitude = longitude + self.title = title + self.identifier = identifier + } +} + +/// Swift interface to the Organic Maps URL Scheme API. +public enum OrganicMaps { + private static let apiVersion = 1 + private static let scheme = "om" + private static let installPageURL = URL(string: "https://omaps.app/get")! + + /// `true` if Organic Maps is installed and can handle `om://` URLs. Requires + /// `om` to be listed under `LSApplicationQueriesSchemes` in your Info.plist. + public static var isInstalled: Bool { + UIApplication.shared.canOpenURL(URL(string: "\(scheme)://")!) + } + + /// Opens the Organic Maps app (no points, just brings it to the foreground). + @discardableResult + public static func openApp() -> Bool { + let url = URL(string: "\(scheme)://")! + guard UIApplication.shared.canOpenURL(url) else { return false } + UIApplication.shared.open(url) + return true + } + + /// Shows one pin on the map. Convenience over `showPins(_:)`. + @discardableResult + public static func showPin(latitude: Double, longitude: Double, + title: String? = nil, identifier: String? = nil) -> Bool { + showPins([OMPin(latitude: latitude, longitude: longitude, title: title, identifier: identifier)]) + } + + /// Shows any number of pins. If Organic Maps isn't installed, presents the + /// "Install Organic Maps" dialog and returns `false`. + @discardableResult + public static func showPins(_ pins: [OMPin], openUrlOnBalloonClick: Bool = false) -> Bool { + guard isInstalled else { + presentInstallDialog() + return false + } + guard !pins.isEmpty, let url = buildMapURL(pins: pins, openUrlOnBalloonClick: openUrlOnBalloonClick) else { + return false + } + UIApplication.shared.open(url) + return true + } + + /// `true` if `url`'s scheme matches the host app's first registered URL scheme, + /// i.e. the callback scheme Organic Maps was told to use. + public static func isOrganicMapsCallback(url: URL) -> Bool { + guard let backScheme = firstRegisteredURLScheme() else { return false } + return url.scheme == backScheme + } + + /// Returns the pin the user selected. `nil` means the user pressed "Back" + /// without selecting a pin, or the URL is malformed. + public static func pin(from url: URL) -> OMPin? { + guard isOrganicMapsCallback(url: url), + url.host == "pin", + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { return nil } + + var lat: Double = .infinity + var lon: Double = .infinity + var title: String? + var identifier: String? + + for item in components.queryItems ?? [] { + guard let value = item.value else { continue } + switch item.name { + case "ll": + let parts = value.split(separator: ",") + if parts.count == 2, let la = Double(parts[0]), let lo = Double(parts[1]) { + lat = la + lon = lo + } + case "n": title = value + case "id": identifier = value + default: break + } + } + guard (-90.0...90.0).contains(lat), (-180.0...180.0).contains(lon) else { return nil } + return OMPin(latitude: lat, longitude: lon, title: title, identifier: identifier) + } + + // MARK: - Internals + + private static func buildMapURL(pins: [OMPin], openUrlOnBalloonClick: Bool) -> URL? { + var components = URLComponents() + components.scheme = scheme + components.host = "map" + + var items: [URLQueryItem] = [URLQueryItem(name: "v", value: "\(apiVersion)")] + if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String, !appName.isEmpty { + items.append(URLQueryItem(name: "appname", value: appName)) + } + if let backScheme = firstRegisteredURLScheme() { + items.append(URLQueryItem(name: "backurl", value: backScheme)) + } + for pin in pins { + // The map parser requires `ll` before `n` and `id` for each point. + items.append(URLQueryItem(name: "ll", value: "\(pin.latitude),\(pin.longitude)")) + if let t = pin.title, !t.isEmpty { items.append(URLQueryItem(name: "n", value: t)) } + if let i = pin.identifier, !i.isEmpty { items.append(URLQueryItem(name: "id", value: i)) } + } + if openUrlOnBalloonClick { + items.append(URLQueryItem(name: "balloonaction", value: "openUrlOnBalloonClick")) + } + components.queryItems = items + return components.url + } + + /// The first scheme listed under CFBundleURLTypes -> CFBundleURLSchemes. Place the + /// scheme you want Organic Maps to call back on first if you register multiple types. + private static func firstRegisteredURLScheme() -> String? { + let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]] ?? [] + for urlType in urlTypes { + if let schemes = urlType["CFBundleURLSchemes"] as? [String], let first = schemes.first { + return first + } + } + print("WARNING: No URL scheme is registered in CFBundleURLTypes. " + + "Add one to allow Organic Maps to return the user to your app.") + return nil + } + + // MARK: - Install dialog + + public static func presentInstallDialog() { + guard let root = keyWindow()?.rootViewController else { return } + let vc = OMInstallDialogController(url: installPageURL) + root.present(UINavigationController(rootViewController: vc), animated: true) + } + + private static func keyWindow() -> UIWindow? { + UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap(\.windows) + .first(where: \.isKeyWindow) + } +} + +private final class OMInstallDialogController: UIViewController, WKNavigationDelegate { + private let url: URL + private lazy var webView: WKWebView = { + let v = WKWebView() + v.navigationDelegate = self + return v + }() + + init(url: URL) { + self.url = url + super.init(nibName: nil, bundle: nil) + title = "Install Organic Maps" + } + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + + override func loadView() { view = webView } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .close, target: self, action: #selector(close)) + webView.load(URLRequest(url: url)) + } + + @objc private func close() { dismiss(animated: true) } + + // Fallback to embedded HTML when omaps.app is unreachable (e.g. offline). + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + webView.loadHTMLString(Self.fallbackHTML, baseURL: nil) + } + + // Route link taps to Safari / App Store so the user can actually install Organic Maps. + // Without this, WKWebView follows the link in-place and nothing visible happens. + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.navigationType == .linkActivated, + let url = navigationAction.request.url { + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + decisionHandler(.allow) + } + + private static let fallbackHTML = """ + + +

Organic Maps is required to continue.

+ Download Organic Maps + """ +} diff --git a/capitals-example/Capitals.xcodeproj/project.pbxproj b/capitals-example/Capitals.xcodeproj/project.pbxproj index eacbe14..181eaff 100644 --- a/capitals-example/Capitals.xcodeproj/project.pbxproj +++ b/capitals-example/Capitals.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 97F3580C185F5BB200DDF84D /* capitals.plist in Resources */ = {isa = PBXBuildFile; fileRef = 97F3580B185F5BB200DDF84D /* capitals.plist */; }; - FA1792CE17784F000092B567 /* MapsWithMeAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = FA1792CC17784F000092B567 /* MapsWithMeAPI.m */; }; + FA0ADFF727B1561E00800A86 /* OrganicMapsAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0ADFF627B1561E00800A86 /* OrganicMapsAPI.m */; }; + FA0ADFF827B1561E00800A86 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA0ADFF927B1561E00800A86 /* WebKit.framework */; }; FA776B4F17848A370023F7A0 /* MasterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA776B4D17848A370023F7A0 /* MasterViewController.xib */; }; FAA484EA178108970027B232 /* 114x114.png in Resources */ = {isa = PBXBuildFile; fileRef = FAA484E2178108970027B232 /* 114x114.png */; }; FAA484EB178108970027B232 /* 144x144.png in Resources */ = {isa = PBXBuildFile; fileRef = FAA484E3178108970027B232 /* 144x144.png */; }; @@ -23,18 +24,16 @@ FAD3DD53177221B500B0735B /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAD3DD52177221B500B0735B /* CoreGraphics.framework */; }; FAD3DD5B177221B500B0735B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DD5A177221B500B0735B /* main.m */; }; FAD3DD5F177221B500B0735B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DD5E177221B500B0735B /* AppDelegate.m */; }; - FAD3DD61177221B500B0735B /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = FAD3DD60177221B500B0735B /* Default.png */; }; - FAD3DD63177221B500B0735B /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FAD3DD62177221B500B0735B /* Default@2x.png */; }; - FAD3DD65177221B500B0735B /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FAD3DD64177221B500B0735B /* Default-568h@2x.png */; }; FAD3DD68177221B500B0735B /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DD67177221B500B0735B /* MasterViewController.m */; }; FAD3DD8317724D4A00B0735B /* CityDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FAD3DD8117724D4A00B0735B /* CityDetailViewController.m */; }; FAD3DD8417724D4A00B0735B /* CityDetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = FAD3DD8217724D4A00B0735B /* CityDetailViewController.xib */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 97F3580B185F5BB200DDF84D /* capitals.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = capitals.plist; sourceTree = ""; }; - FA1792CC17784F000092B567 /* MapsWithMeAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MapsWithMeAPI.m; path = ../api/MapsWithMeAPI.m; sourceTree = ""; }; - FA1792CD17784F000092B567 /* MapsWithMeAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MapsWithMeAPI.h; path = ../api/MapsWithMeAPI.h; sourceTree = ""; }; + 97F3580B185F5BB200DDF84D /* capitals.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = capitals.plist; path = "../shared-resources/capitals.plist"; sourceTree = SOURCE_ROOT; }; + FA0ADFF527B1561E00800A86 /* OrganicMapsAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OrganicMapsAPI.h; path = ../api/OrganicMapsAPI.h; sourceTree = ""; }; + FA0ADFF627B1561E00800A86 /* OrganicMapsAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OrganicMapsAPI.m; path = ../api/OrganicMapsAPI.m; sourceTree = ""; }; + FA0ADFF927B1561E00800A86 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; FA776B4D17848A370023F7A0 /* MasterViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MasterViewController.xib; sourceTree = ""; }; FAA484E2178108970027B232 /* 114x114.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 114x114.png; sourceTree = ""; }; FAA484E3178108970027B232 /* 144x144.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 144x144.png; sourceTree = ""; }; @@ -53,9 +52,6 @@ FAD3DD5C177221B500B0735B /* Capitals-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Capitals-Prefix.pch"; sourceTree = ""; }; FAD3DD5D177221B500B0735B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; FAD3DD5E177221B500B0735B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - FAD3DD60177221B500B0735B /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; - FAD3DD62177221B500B0735B /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; - FAD3DD64177221B500B0735B /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; FAD3DD66177221B500B0735B /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; }; FAD3DD67177221B500B0735B /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; }; FAD3DD8017724D4A00B0735B /* CityDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CityDetailViewController.h; sourceTree = ""; }; @@ -71,25 +67,26 @@ FAD3DD4F177221B500B0735B /* UIKit.framework in Frameworks */, FAD3DD51177221B500B0735B /* Foundation.framework in Frameworks */, FAD3DD53177221B500B0735B /* CoreGraphics.framework in Frameworks */, + FA0ADFF827B1561E00800A86 /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - FA1792C7177843650092B567 /* MapsWithMe API */ = { + FA1792C7177843650092B567 /* Organic Maps API */ = { isa = PBXGroup; children = ( - FA1792CD17784F000092B567 /* MapsWithMeAPI.h */, - FA1792CC17784F000092B567 /* MapsWithMeAPI.m */, + FA0ADFF527B1561E00800A86 /* OrganicMapsAPI.h */, + FA0ADFF627B1561E00800A86 /* OrganicMapsAPI.m */, ); - name = "MapsWithMe API"; + name = "Organic Maps API"; sourceTree = ""; }; FAD3DD42177221B500B0735B = { isa = PBXGroup; children = ( - FA1792C7177843650092B567 /* MapsWithMe API */, + FA1792C7177843650092B567 /* Organic Maps API */, FAD3DD54177221B500B0735B /* Capitals */, FAD3DD4D177221B500B0735B /* Frameworks */, FAD3DD4C177221B500B0735B /* Products */, @@ -110,6 +107,7 @@ FAD3DD4E177221B500B0735B /* UIKit.framework */, FAD3DD50177221B500B0735B /* Foundation.framework */, FAD3DD52177221B500B0735B /* CoreGraphics.framework */, + FA0ADFF927B1561E00800A86 /* WebKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -145,9 +143,6 @@ FAD3DD56177221B500B0735B /* Capitals-Info.plist */, FAD3DD5A177221B500B0735B /* main.m */, FAD3DD5C177221B500B0735B /* Capitals-Prefix.pch */, - FAD3DD60177221B500B0735B /* Default.png */, - FAD3DD62177221B500B0735B /* Default@2x.png */, - FAD3DD64177221B500B0735B /* Default-568h@2x.png */, ); name = "Supporting Files"; sourceTree = ""; @@ -178,15 +173,17 @@ FAD3DD43177221B500B0735B /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0720; - ORGANIZATIONNAME = "MapsWithMe GmbH"; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 2650; + ORGANIZATIONNAME = "Organic Maps GmbH"; }; buildConfigurationList = FAD3DD46177221B500B0735B /* Build configuration list for PBXProject "Capitals" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = FAD3DD42177221B500B0735B; productRefGroup = FAD3DD4C177221B500B0735B /* Products */; @@ -203,10 +200,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FAD3DD61177221B500B0735B /* Default.png in Resources */, 97F3580C185F5BB200DDF84D /* capitals.plist in Resources */, - FAD3DD63177221B500B0735B /* Default@2x.png in Resources */, - FAD3DD65177221B500B0735B /* Default-568h@2x.png in Resources */, FAD3DD8417724D4A00B0735B /* CityDetailViewController.xib in Resources */, FAA484EA178108970027B232 /* 114x114.png in Resources */, FAA484EB178108970027B232 /* 144x144.png in Resources */, @@ -229,9 +223,9 @@ files = ( FAD3DD5B177221B500B0735B /* main.m in Sources */, FAD3DD5F177221B500B0735B /* AppDelegate.m in Sources */, + FA0ADFF727B1561E00800A86 /* OrganicMapsAPI.m in Sources */, FAD3DD68177221B500B0735B /* MasterViewController.m in Sources */, FAD3DD8317724D4A00B0735B /* CityDetailViewController.m in Sources */, - FA1792CE17784F000092B567 /* MapsWithMeAPI.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -242,22 +236,44 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LIBRARY = "libstdc++"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COPY_PHASE_STRIP = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; PRODUCT_NAME = ""; PROVISIONING_PROFILE = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -270,8 +286,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Capitals/Capitals-Prefix.pch"; INFOPLIST_FILE = "Capitals/Capitals-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mapswithme.api.example.Capitals; + PRODUCT_BUNDLE_IDENTIFIER = app.organicmaps.api.example.Capitals; PRODUCT_NAME = "World Capitals"; WRAPPER_EXTENSION = app; }; @@ -281,29 +296,51 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LIBRARY = "libstdc++"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -312,21 +349,43 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LIBRARY = "libstdc++"; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; PRODUCT_NAME = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -336,12 +395,14 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Capitals/Capitals-Prefix.pch"; INFOPLIST_FILE = "Capitals/Capitals-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mapswithme.api.example.Capitals; + PRODUCT_BUNDLE_IDENTIFIER = app.organicmaps.api.example.Capitals; PRODUCT_NAME = "World Capitals"; + PROVISIONING_PROFILE_SPECIFIER = ""; WRAPPER_EXTENSION = app; }; name = Debug; @@ -350,12 +411,14 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Capitals/Capitals-Prefix.pch"; INFOPLIST_FILE = "Capitals/Capitals-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mapswithme.api.example.Capitals; + PRODUCT_BUNDLE_IDENTIFIER = app.organicmaps.api.example.Capitals; PRODUCT_NAME = "World Capitals"; + PROVISIONING_PROFILE_SPECIFIER = ""; WRAPPER_EXTENSION = app; }; name = Release; diff --git a/capitals-example/Capitals/100x100.png b/capitals-example/Capitals/100x100.png index dd5b0ac..aa885b2 100644 Binary files a/capitals-example/Capitals/100x100.png and b/capitals-example/Capitals/100x100.png differ diff --git a/capitals-example/Capitals/114x114.png b/capitals-example/Capitals/114x114.png index 73558ad..5a0b4f4 100644 Binary files a/capitals-example/Capitals/114x114.png and b/capitals-example/Capitals/114x114.png differ diff --git a/capitals-example/Capitals/144x144.png b/capitals-example/Capitals/144x144.png index 00b29ef..51772ad 100644 Binary files a/capitals-example/Capitals/144x144.png and b/capitals-example/Capitals/144x144.png differ diff --git a/capitals-example/Capitals/29x29.png b/capitals-example/Capitals/29x29.png index cbb50fa..9648d0f 100644 Binary files a/capitals-example/Capitals/29x29.png and b/capitals-example/Capitals/29x29.png differ diff --git a/capitals-example/Capitals/50x50.png b/capitals-example/Capitals/50x50.png index 9b713a3..64ddd01 100644 Binary files a/capitals-example/Capitals/50x50.png and b/capitals-example/Capitals/50x50.png differ diff --git a/capitals-example/Capitals/57x57.png b/capitals-example/Capitals/57x57.png index 96c780b..939aa4c 100644 Binary files a/capitals-example/Capitals/57x57.png and b/capitals-example/Capitals/57x57.png differ diff --git a/capitals-example/Capitals/58x58.png b/capitals-example/Capitals/58x58.png index b8fa5af..ff75455 100644 Binary files a/capitals-example/Capitals/58x58.png and b/capitals-example/Capitals/58x58.png differ diff --git a/capitals-example/Capitals/72x72.png b/capitals-example/Capitals/72x72.png index 12496a7..a85aa82 100644 Binary files a/capitals-example/Capitals/72x72.png and b/capitals-example/Capitals/72x72.png differ diff --git a/capitals-example/Capitals/AppDelegate.h b/capitals-example/Capitals/AppDelegate.h index 883be08..7626275 100644 --- a/capitals-example/Capitals/AppDelegate.h +++ b/capitals-example/Capitals/AppDelegate.h @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/capitals-example/Capitals/AppDelegate.m b/capitals-example/Capitals/AppDelegate.m index bd6137d..4ace2f4 100644 --- a/capitals-example/Capitals/AppDelegate.m +++ b/capitals-example/Capitals/AppDelegate.m @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -30,17 +30,17 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE #import "MasterViewController.h" #import "CityDetailViewController.h" -#import "MapsWithMeAPI.h" +#import "OrganicMapsAPI.h" @implementation AppDelegate -// MapsWithMe API entry point, when user comes back to your app -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +// Organic Maps API entry point, when user comes back to your app +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { - if ([MWMApi isMapsWithMeUrl:url]) + if ([OMApi isOrganicMapsUrl:url]) { // if we got nil, it means that Back button was pressed without selecting any pin - MWMPin * pin = [MWMApi pinFromUrl:url]; + OMPin * pin = [OMApi pinFromUrl:url]; if (pin) { NSInteger const cityId = [pin.idOrUrl integerValue]; diff --git a/capitals-example/Capitals/Capitals-Info.plist b/capitals-example/Capitals/Capitals-Info.plist index b91527d..dd31589 100644 --- a/capitals-example/Capitals/Capitals-Info.plist +++ b/capitals-example/Capitals/Capitals-Info.plist @@ -35,10 +35,10 @@ CFBundleTypeRole Viewer CFBundleURLName - com.mapswithme.maps + app.organicmaps CFBundleURLSchemes - MapsWithMeApiExampleCapitals + OMApiExampleCapitals @@ -46,7 +46,7 @@ 1.0 LSApplicationQueriesSchemes - mapswithme + om LSRequiresIPhoneOS diff --git a/capitals-example/Capitals/Capitals-Prefix.pch b/capitals-example/Capitals/Capitals-Prefix.pch index f497d6b..edecf91 100644 --- a/capitals-example/Capitals/Capitals-Prefix.pch +++ b/capitals-example/Capitals/Capitals-Prefix.pch @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -28,10 +28,6 @@ #import -#ifndef __IPHONE_4_3 -#warning "This project uses features only available in iOS SDK 4.3 and later." -#endif - #ifdef __OBJC__ #import #import diff --git a/capitals-example/Capitals/CityDetailViewController.h b/capitals-example/Capitals/CityDetailViewController.h index 4666236..f7242e3 100644 --- a/capitals-example/Capitals/CityDetailViewController.h +++ b/capitals-example/Capitals/CityDetailViewController.h @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/capitals-example/Capitals/CityDetailViewController.m b/capitals-example/Capitals/CityDetailViewController.m index 64e2fc3..db3a902 100644 --- a/capitals-example/Capitals/CityDetailViewController.m +++ b/capitals-example/Capitals/CityDetailViewController.m @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -28,7 +28,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE #import "CityDetailViewController.h" -#import "MapsWithMeAPI.h" +#import "OrganicMapsAPI.h" @interface CityDetailViewController () @@ -51,7 +51,7 @@ - (void)showCapitalOnTheMap:(BOOL)withLink pinId = [NSString stringWithFormat:@"http://en.wikipedia.org/wiki/%@", [self urlEncode:self.city[@"name"]]]; else pinId = [NSString stringWithFormat:@"%@", @(_cityIndex)]; - [MWMApi showLat:[self.city[@"lat"] doubleValue] lon:[self.city[@"lon"] doubleValue] title:self.city[@"name"] idOrUrl:pinId]; + [OMApi showLat:[self.city[@"lat"] doubleValue] lon:[self.city[@"lon"] doubleValue] title:self.city[@"name"] idOrUrl:pinId]; } - (void)setCityIndex:(NSInteger)newCityIndex diff --git a/capitals-example/Capitals/Default-568h@2x.png b/capitals-example/Capitals/Default-568h@2x.png deleted file mode 100644 index cc9e79c..0000000 Binary files a/capitals-example/Capitals/Default-568h@2x.png and /dev/null differ diff --git a/capitals-example/Capitals/Default.png b/capitals-example/Capitals/Default.png deleted file mode 100644 index 126d426..0000000 Binary files a/capitals-example/Capitals/Default.png and /dev/null differ diff --git a/capitals-example/Capitals/Default@2x.png b/capitals-example/Capitals/Default@2x.png deleted file mode 100644 index 6468cbe..0000000 Binary files a/capitals-example/Capitals/Default@2x.png and /dev/null differ diff --git a/capitals-example/Capitals/MasterViewController.h b/capitals-example/Capitals/MasterViewController.h index 7dfff7a..faf8efd 100644 --- a/capitals-example/Capitals/MasterViewController.h +++ b/capitals-example/Capitals/MasterViewController.h @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/capitals-example/Capitals/MasterViewController.m b/capitals-example/Capitals/MasterViewController.m index 9b179e6..eb9241d 100644 --- a/capitals-example/Capitals/MasterViewController.m +++ b/capitals-example/Capitals/MasterViewController.m @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,7 +29,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE #import "MasterViewController.h" #import "CityDetailViewController.h" -#import "MapsWithMeAPI.h" +#import "OrganicMapsAPI.h" @implementation MasterViewController @@ -52,21 +52,21 @@ - (NSArray *)capitals - (void)showAllCitiesOnTheMap:(id)sender { - NSMutableArray * array = [[NSMutableArray alloc] initWithCapacity:[self.capitals count]]; + NSMutableArray * array = [[NSMutableArray alloc] initWithCapacity:[self.capitals count]]; for (NSInteger i = 0; i < [self.capitals count]; ++i) { NSString * pinId = [NSString stringWithFormat:@"%@", @(i)]; - // Note that url is empty - it means "More details" button for a pin in MapsWithMe will lead back to this example app + // Note that url is empty - it means "More details" button for a pin in Organic Maps will lead back to this example app NSDictionary * city = self.capitals[i]; - MWMPin * pin = [[MWMPin alloc] initWithLat:[city[@"lat"] doubleValue] lon:[city[@"lon"] doubleValue] title:city[@"name"] idOrUrl:pinId]; + OMPin * pin = [[OMPin alloc] initWithLat:[city[@"lat"] doubleValue] lon:[city[@"lon"] doubleValue] title:city[@"name"] idOrUrl:pinId]; [array addObject:pin]; } // Your should hide any top view objects like UIPopoverController before calling +showPins: - // If user does not installed MapsWithMe app, a popup dialog will be shown + // If user does not installed Organic Maps app, a popup dialog will be shown [self.detailViewController.masterPopoverController dismissPopoverAnimated:YES]; - - [MWMApi showPins:array]; + + [OMApi showPins:array]; } - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil @@ -107,7 +107,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 240, tableView.rowHeight)]; - label.text = [MWMApi isApiSupported] ? @"MapsWithMe is installed" : @"MapsWithMe is not installed"; + label.text = [OMApi isApiSupported] ? @"Organic Maps is installed" : @"Organic Maps is not installed"; label.textAlignment = NSTextAlignmentCenter; label.backgroundColor = [UIColor clearColor]; return label; diff --git a/capitals-example/Capitals/main.m b/capitals-example/Capitals/main.m index e00c54b..2fcf64f 100644 --- a/capitals-example/Capitals/main.m +++ b/capitals-example/Capitals/main.m @@ -1,6 +1,6 @@ /******************************************************************************* - Copyright (c) 2013, MapsWithMe GmbH + Copyright (c) 2026, Organic Maps OÜ All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/capitals-example/Capitals/capitals.plist b/shared-resources/capitals.plist similarity index 100% rename from capitals-example/Capitals/capitals.plist rename to shared-resources/capitals.plist diff --git a/site-resources/add_custom_url_scheme.png b/site-resources/add_custom_url_scheme.png deleted file mode 100644 index d195087..0000000 Binary files a/site-resources/add_custom_url_scheme.png and /dev/null differ diff --git a/site-resources/download_mwm_dialog.png b/site-resources/download_mwm_dialog.png deleted file mode 100644 index ecd0349..0000000 Binary files a/site-resources/download_mwm_dialog.png and /dev/null differ diff --git a/swift-capitals-example/Capitals.xcodeproj/project.pbxproj b/swift-capitals-example/Capitals.xcodeproj/project.pbxproj new file mode 100644 index 0000000..00cd838 --- /dev/null +++ b/swift-capitals-example/Capitals.xcodeproj/project.pbxproj @@ -0,0 +1,348 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A0010001000000000000AAAA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0020001000000000000AAAA /* AppDelegate.swift */; }; + A0010002000000000000AAAA /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0020002000000000000AAAA /* MasterViewController.swift */; }; + A0010003000000000000AAAA /* CityDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0020003000000000000AAAA /* CityDetailViewController.swift */; }; + A0010004000000000000AAAA /* Capital.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0020004000000000000AAAA /* Capital.swift */; }; + A0010005000000000000AAAA /* OrganicMapsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0020005000000000000AAAA /* OrganicMapsAPI.swift */; }; + A0010006000000000000AAAA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A0020006000000000000AAAA /* LaunchScreen.storyboard */; }; + A0010007000000000000AAAA /* capitals.plist in Resources */ = {isa = PBXBuildFile; fileRef = A0020007000000000000AAAA /* capitals.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A0020001000000000000AAAA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A0020002000000000000AAAA /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; + A0020003000000000000AAAA /* CityDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityDetailViewController.swift; sourceTree = ""; }; + A0020004000000000000AAAA /* Capital.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capital.swift; sourceTree = ""; }; + A0020005000000000000AAAA /* OrganicMapsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OrganicMapsAPI.swift; path = ../api/OrganicMapsAPI.swift; sourceTree = ""; }; + A0020006000000000000AAAA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + A0020007000000000000AAAA /* capitals.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = capitals.plist; path = ../shared-resources/capitals.plist; sourceTree = SOURCE_ROOT; }; + A0020008000000000000AAAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A0020009000000000000AAAA /* World Capitals Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "World Capitals Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A0030001000000000000AAAA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A0040001000000000000AAAA = { + isa = PBXGroup; + children = ( + A0040002000000000000AAAA /* Organic Maps API */, + A0040003000000000000AAAA /* Capitals */, + A0040004000000000000AAAA /* Products */, + ); + sourceTree = ""; + }; + A0040002000000000000AAAA /* Organic Maps API */ = { + isa = PBXGroup; + children = ( + A0020005000000000000AAAA /* OrganicMapsAPI.swift */, + ); + name = "Organic Maps API"; + sourceTree = ""; + }; + A0040003000000000000AAAA /* Capitals */ = { + isa = PBXGroup; + children = ( + A0020001000000000000AAAA /* AppDelegate.swift */, + A0020002000000000000AAAA /* MasterViewController.swift */, + A0020003000000000000AAAA /* CityDetailViewController.swift */, + A0020004000000000000AAAA /* Capital.swift */, + A0040005000000000000AAAA /* Supporting Files */, + ); + path = Capitals; + sourceTree = ""; + }; + A0040004000000000000AAAA /* Products */ = { + isa = PBXGroup; + children = ( + A0020009000000000000AAAA /* World Capitals Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + A0040005000000000000AAAA /* Supporting Files */ = { + isa = PBXGroup; + children = ( + A0020008000000000000AAAA /* Info.plist */, + A0020006000000000000AAAA /* LaunchScreen.storyboard */, + A0020007000000000000AAAA /* capitals.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A0050001000000000000AAAA /* Capitals */ = { + isa = PBXNativeTarget; + buildConfigurationList = A0060001000000000000AAAA /* Build configuration list for PBXNativeTarget "Capitals" */; + buildPhases = ( + A0070001000000000000AAAA /* Sources */, + A0030001000000000000AAAA /* Frameworks */, + A0070002000000000000AAAA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Capitals; + productName = Capitals; + productReference = A0020009000000000000AAAA /* World Capitals Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A0080001000000000000AAAA /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1500; + ORGANIZATIONNAME = "Organic Maps OÜ"; + TargetAttributes = { + A0050001000000000000AAAA = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = A0060002000000000000AAAA /* Build configuration list for PBXProject "Capitals" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A0040001000000000000AAAA; + productRefGroup = A0040004000000000000AAAA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A0050001000000000000AAAA /* Capitals */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A0070002000000000000AAAA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A0010006000000000000AAAA /* LaunchScreen.storyboard in Resources */, + A0010007000000000000AAAA /* capitals.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A0070001000000000000AAAA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A0010001000000000000AAAA /* AppDelegate.swift in Sources */, + A0010002000000000000AAAA /* MasterViewController.swift in Sources */, + A0010003000000000000AAAA /* CityDetailViewController.swift in Sources */, + A0010004000000000000AAAA /* Capital.swift in Sources */, + A0010005000000000000AAAA /* OrganicMapsAPI.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A0090001000000000000AAAA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A0090002000000000000AAAA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A0090003000000000000AAAA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Capitals/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.organicmaps.api.example.CapitalsSwift; + PRODUCT_NAME = "World Capitals Swift"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A0090004000000000000AAAA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Capitals/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.organicmaps.api.example.CapitalsSwift; + PRODUCT_NAME = "World Capitals Swift"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A0060001000000000000AAAA /* Build configuration list for PBXNativeTarget "Capitals" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A0090003000000000000AAAA /* Debug */, + A0090004000000000000AAAA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A0060002000000000000AAAA /* Build configuration list for PBXProject "Capitals" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A0090001000000000000AAAA /* Debug */, + A0090002000000000000AAAA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A0080001000000000000AAAA /* Project object */; +} diff --git a/swift-capitals-example/Capitals.xcodeproj/project.xcworkspace/xcuserdata/alex.xcuserdatad/UserInterfaceState.xcuserstate b/swift-capitals-example/Capitals.xcodeproj/project.xcworkspace/xcuserdata/alex.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..4390086 Binary files /dev/null and b/swift-capitals-example/Capitals.xcodeproj/project.xcworkspace/xcuserdata/alex.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/swift-capitals-example/Capitals.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist b/swift-capitals-example/Capitals.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..ea17f6e --- /dev/null +++ b/swift-capitals-example/Capitals.xcodeproj/xcuserdata/alex.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Capitals.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/swift-capitals-example/Capitals/AppDelegate.swift b/swift-capitals-example/Capitals/AppDelegate.swift new file mode 100644 index 0000000..b50d247 --- /dev/null +++ b/swift-capitals-example/Capitals/AppDelegate.swift @@ -0,0 +1,62 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +import UIKit + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + let master = MasterViewController(style: .plain) + let nav = UINavigationController(rootViewController: master) + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = nav + window.makeKeyAndVisible() + self.window = window + return true + } + + // Organic Maps API entry point — called when the user returns from Organic Maps. + func application(_ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + guard OrganicMaps.isOrganicMapsCallback(url: url) else { return false } + + // pin == nil means the user pressed "Back" without selecting any pin. + if let pin = OrganicMaps.pin(from: url), + let nav = window?.rootViewController as? UINavigationController, + let master = nav.viewControllers.first as? MasterViewController, + let index = Int(pin.identifier ?? "") { + nav.popToRootViewController(animated: false) + master.pushDetail(for: index, animated: true) + } + return true + } +} diff --git a/swift-capitals-example/Capitals/Capital.swift b/swift-capitals-example/Capitals/Capital.swift new file mode 100644 index 0000000..52edb4f --- /dev/null +++ b/swift-capitals-example/Capitals/Capital.swift @@ -0,0 +1,54 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +import Foundation + +struct Capital { + let name: String + let latitude: Double + let longitude: Double + + init?(from dict: [String: Any]) { + guard let name = dict["name"] as? String, + let latitude = dict["lat"] as? Double, + let longitude = dict["lon"] as? Double + else { return nil } + self.name = name + self.latitude = latitude + self.longitude = longitude + } + + /// Loads the bundled capitals.plist (shared with the Obj-C example). + static func loadAll() -> [Capital] { + guard let url = Bundle.main.url(forResource: "capitals", withExtension: "plist"), + let data = try? Data(contentsOf: url), + let list = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [[String: Any]] + else { return [] } + return list.compactMap(Capital.init(from:)) + } +} diff --git a/swift-capitals-example/Capitals/CityDetailViewController.swift b/swift-capitals-example/Capitals/CityDetailViewController.swift new file mode 100644 index 0000000..08b0989 --- /dev/null +++ b/swift-capitals-example/Capitals/CityDetailViewController.swift @@ -0,0 +1,103 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +import UIKit + +final class CityDetailViewController: UIViewController { + private let capital: Capital + private let index: Int + + init(capital: Capital, index: Int) { + self.capital = capital + self.index = index + super.init(nibName: nil, bundle: nil) + title = capital.name + } + + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + let nameLabel = UILabel() + nameLabel.text = capital.name + nameLabel.font = .preferredFont(forTextStyle: .title1) + nameLabel.textAlignment = .center + + let coordsLabel = UILabel() + coordsLabel.text = String(format: "lat: %.4f, lon: %.4f", capital.latitude, capital.longitude) + coordsLabel.font = .preferredFont(forTextStyle: .body) + coordsLabel.textColor = .secondaryLabel + coordsLabel.textAlignment = .center + + let mapButton = UIButton(type: .system) + mapButton.setTitle("Show on map", for: .normal) + mapButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + mapButton.addTarget(self, action: #selector(showOnMap), for: .touchUpInside) + + let wikiButton = UIButton(type: .system) + wikiButton.setTitle("Show with Wikipedia link", for: .normal) + wikiButton.titleLabel?.font = .preferredFont(forTextStyle: .headline) + wikiButton.addTarget(self, action: #selector(showWithWikipediaLink), for: .touchUpInside) + + let stack = UIStackView(arrangedSubviews: [nameLabel, coordsLabel, mapButton, wikiButton]) + stack.axis = .vertical + stack.spacing = 24 + stack.alignment = .center + stack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), + stack.leadingAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + stack.trailingAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), + ]) + } + + @objc private func showOnMap() { + OrganicMaps.showPin( + latitude: capital.latitude, + longitude: capital.longitude, + title: capital.name, + identifier: "\(index)") + } + + @objc private func showWithWikipediaLink() { + let urlSafeName = capital.name.replacingOccurrences(of: " ", with: "_") + let wikipediaURL = "https://en.wikipedia.org/wiki/\(urlSafeName)" + OrganicMaps.showPin( + latitude: capital.latitude, + longitude: capital.longitude, + title: capital.name, + // A valid URL in `identifier` makes Organic Maps' "More Info" open the page + // instead of returning to our app. + identifier: wikipediaURL) + } +} diff --git a/swift-capitals-example/Capitals/Info.plist b/swift-capitals-example/Capitals/Info.plist new file mode 100644 index 0000000..52707a3 --- /dev/null +++ b/swift-capitals-example/Capitals/Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CFBundleURLTypes + + + CFBundleURLName + app.organicmaps.api.example.CapitalsSwift + CFBundleURLSchemes + + OMApiExampleCapitalsSwift + + + + LSApplicationQueriesSchemes + + om + + + diff --git a/swift-capitals-example/Capitals/LaunchScreen.storyboard b/swift-capitals-example/Capitals/LaunchScreen.storyboard new file mode 100644 index 0000000..11f87a6 --- /dev/null +++ b/swift-capitals-example/Capitals/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift-capitals-example/Capitals/MasterViewController.swift b/swift-capitals-example/Capitals/MasterViewController.swift new file mode 100644 index 0000000..bbb436b --- /dev/null +++ b/swift-capitals-example/Capitals/MasterViewController.swift @@ -0,0 +1,91 @@ +/******************************************************************************* + + Copyright (c) 2026, Organic Maps OÜ + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ******************************************************************************/ + +import UIKit + +final class MasterViewController: UITableViewController { + private static let cellID = "CapitalCell" + + let capitals: [Capital] = Capital.loadAll() + + override func viewDidLoad() { + super.viewDidLoad() + title = "World Capitals" + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Self.cellID) + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Show All", style: .plain, target: self, action: #selector(showAllOnMap)) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Status updates whenever the user returns to this screen — Organic Maps may + // have been installed/uninstalled while the app was in the background. + tableView.reloadSections(IndexSet(integer: 0), with: .none) + } + + @objc private func showAllOnMap() { + let pins = capitals.enumerated().map { index, city in + OMPin(latitude: city.latitude, longitude: city.longitude, + title: city.name, + // The id is echoed back on pin selection; we'll use the city index + // to look up the right detail screen in AppDelegate. + identifier: "\(index)") + } + OrganicMaps.showPins(pins) + } + + func pushDetail(for index: Int, animated: Bool) { + guard capitals.indices.contains(index) else { return } + let detail = CityDetailViewController(capital: capitals[index], index: index) + navigationController?.pushViewController(detail, animated: animated) + } + + // MARK: - UITableViewDataSource / Delegate + + override func numberOfSections(in tableView: UITableView) -> Int { 1 } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + OrganicMaps.isInstalled ? "Organic Maps is installed" : "Organic Maps is not installed" + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + capitals.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellID, for: indexPath) + cell.textLabel?.text = capitals[indexPath.row].name + cell.accessoryType = .disclosureIndicator + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + pushDetail(for: indexPath.row, animated: true) + } +}