├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── ArcGISToolkit.xcscheme ├── Documentation ├── AR │ └── README.md ├── Bookmarks │ └── README.md ├── Compass │ └── README.md ├── FloorFilter │ └── README.md ├── JobManager │ └── README.md ├── LegendViewController │ └── README.md ├── MeasureToolbar │ └── README.md ├── PopupController │ └── README.md ├── README.md ├── Scalebar │ ├── Images │ │ ├── alternating-bar.png │ │ ├── bar.png │ │ ├── dual-unit-line.png │ │ ├── graduated-line.png │ │ └── line.png │ └── README.md ├── TemplatePicker │ └── README.md └── TimeSlider │ ├── Images │ ├── black.png │ ├── blue.png │ ├── end-time-pinned.png │ ├── labelmode-thumbs.png │ ├── labelmode-ticks.png │ ├── ocean-blue.png │ ├── only-playback-buttons-visible.png │ ├── only-slider-visible.png │ ├── start-time-pinned.png │ ├── time-instant.png │ ├── time-window.png │ └── timeslider.png │ └── README.md ├── Examples ├── .swiftlint.yml ├── ArcGISToolkitExamples.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── ArcGISToolkitExamples │ ├── ARExample.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── BookmarksExample.swift │ ├── CompassExample.swift │ ├── ExamplesViewController.swift │ ├── FloorFilterExample.swift │ ├── Info.plist │ ├── JobManagerExample.swift │ ├── LegendExample.swift │ ├── MeasureExample.swift │ ├── Misc │ │ ├── ARStatusViewController.storyboard │ │ ├── ARStatusViewController.swift │ │ ├── CalibrationView.swift │ │ ├── Plane.swift │ │ └── UserDirectionsView.swift │ ├── PopupExample.swift │ ├── ScalebarExample.swift │ ├── TemplatePickerExample.swift │ ├── TimeSliderExample.swift │ └── VCListViewController.swift ├── README.md └── exportOptions.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ArcGISToolkit │ ├── AR │ └── ArcGISARView.swift │ ├── BookmarksTableViewController.swift │ ├── BookmarksViewController.swift │ ├── CancelGroup.swift │ ├── Coalescer.swift │ ├── Compass.swift │ ├── Extensions.swift │ ├── FloorFilter │ ├── FloorFilterViewController.swift │ ├── FloorFilterViewModel.swift │ ├── SiteFacilityPromptViewController.swift │ └── SiteFacilityTableViewCell.swift │ ├── JobManager.swift │ ├── LegendViewController.swift │ ├── MapViewController.swift │ ├── MeasureToolbar.swift │ ├── PopupController.swift │ ├── Resources │ ├── Assets.xcassets │ │ ├── Back.imageset │ │ │ ├── Backward132.png │ │ │ ├── Backward44.png │ │ │ ├── Backward88.png │ │ │ └── Contents.json │ │ ├── CompassIcon.imageset │ │ │ ├── Contents.json │ │ │ ├── iOS8_NorthCompass108.png │ │ │ ├── iOS8_NorthCompass36_D.png │ │ │ └── iOS8_NorthCompass72.png │ │ ├── Contents.json │ │ ├── Dot.imageset │ │ │ ├── Contents.json │ │ │ └── fiber_manual_record_black_18dp.svg │ │ ├── Forward.imageset │ │ │ ├── Contents.json │ │ │ ├── Forward132.png │ │ │ ├── Forward44.png │ │ │ └── Forward88.png │ │ ├── MeasureArea.imageset │ │ │ ├── Contents.json │ │ │ ├── iOS8_Toolbar_MeasureArea22.png │ │ │ ├── iOS8_Toolbar_MeasureArea44.png │ │ │ └── iOS8_Toolbar_MeasureArea66.png │ │ ├── MeasureFeature.imageset │ │ │ ├── Contents.json │ │ │ ├── MeasureFeature22.png │ │ │ ├── MeasureFeature44.png │ │ │ └── MeasureFeature66.png │ │ ├── MeasureLength.imageset │ │ │ ├── Contents.json │ │ │ ├── iOS8_Toolbar_MeasureLength22.png │ │ │ ├── iOS8_Toolbar_MeasureLength44.png │ │ │ └── iOS8_Toolbar_MeasureLength66.png │ │ ├── Pause.imageset │ │ │ ├── Contents.json │ │ │ ├── Pause132.png │ │ │ ├── Pause44.png │ │ │ └── Pause88.png │ │ ├── Play.imageset │ │ │ ├── Contents.json │ │ │ ├── Play132.png │ │ │ ├── Play44.png │ │ │ └── Play88.png │ │ ├── Redo.imageset │ │ │ ├── Contents.json │ │ │ ├── iOS8_TabBar_Redo22s.png │ │ │ ├── iOS8_TabBar_Redo44s.png │ │ │ └── iOS8_TabBar_Redo66s.png │ │ ├── Site.imageset │ │ │ ├── Contents.json │ │ │ ├── business_black_18dp (1).svg │ │ │ ├── business_black_24dp (1)-1.svg │ │ │ └── business_black_24dp (1).svg │ │ └── Undo.imageset │ │ │ ├── Contents.json │ │ │ ├── iOS8_TabBar_Undo22s.png │ │ │ ├── iOS8_TabBar_Undo44s.png │ │ │ └── iOS8_TabBar_Undo66s.png │ ├── FloorFilter.storyboard │ ├── FloorFilterSiteFacilityCell.xib │ ├── Legend.storyboard │ └── PrivacyInfo.xcprivacy │ ├── Scalebar.swift │ ├── TableViewController.swift │ ├── TemplatePickerViewController.swift │ ├── TimeSlider.swift │ └── UnitsViewController.swift └── Tests └── ArcGISToolkitTests ├── ArcGISToolkitTests.swift ├── BookmarksViewControllerTests.swift └── FloorFilterViewModelTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Filesystem 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | Package.resolved 43 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - anyobject_protocol 3 | - array_init 4 | - attributes 5 | - block_based_kvo 6 | - closure_end_indentation 7 | - closure_spacing 8 | - collection_alignment 9 | - contains_over_filter_count 10 | - contains_over_filter_is_empty 11 | - contains_over_first_not_nil 12 | - convenience_type 13 | - discouraged_direct_init 14 | - discouraged_optional_boolean 15 | - empty_collection_literal 16 | - empty_count 17 | - empty_string 18 | - empty_xctest_method 19 | - explicit_init 20 | - extension_access_modifier 21 | - fatal_error_message 22 | - first_where 23 | - function_default_parameter_at_end 24 | - identical_operands 25 | - joined_default_parameter 26 | - legacy_random 27 | - let_var_whitespace 28 | - literal_expression_end_indentation 29 | - lower_acl_than_parent 30 | - modifier_order 31 | - multiline_arguments 32 | - multiline_function_chains 33 | - multiline_parameters 34 | - operator_usage_whitespace 35 | - operator_whitespace 36 | - overridden_super_call 37 | - override_in_extension 38 | - prohibited_super_call 39 | - redundant_nil_coalescing 40 | - redundant_type_annotation 41 | - sorted_first_last 42 | - static_operator 43 | - toggle_bool 44 | - trailing_closure 45 | - untyped_error_in_catch 46 | - vertical_parameter_alignment_on_call 47 | - vertical_whitespace_closing_braces 48 | - vertical_whitespace_opening_braces 49 | - xctfail_message 50 | - yoda_condition 51 | disabled_rules: 52 | - file_length 53 | - for_where 54 | - force_cast 55 | - function_body_length 56 | - function_parameter_count 57 | - identifier_name 58 | - large_tuple 59 | - line_length 60 | - nesting 61 | - todo 62 | - type_body_length 63 | 64 | trailing_whitespace: 65 | ignores_empty_lines: true 66 | 67 | excluded: 68 | - Pods 69 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ArcGISToolkit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Documentation/AR/README.md: -------------------------------------------------------------------------------- 1 | # Augmented reality (AR) 2 | 3 | [![guide doc](https://img.shields.io/badge/Full_Developers_Guide-Doc-purple)](https://developers.arcgis.com/ios/scenes-3d/display-scenes-in-augmented-reality/) [![world-scale sample](https://img.shields.io/badge/World_Scale-Sample-blue)](https://developers.arcgis.com/ios/swift/sample-code/collect-data-in-ar/) [![Tabletop sample](https://img.shields.io/badge/Tabletop-Sample-blue)](https://developers.arcgis.com/ios/swift/sample-code/display-scenes-in-tabletop-ar/) [![Flyover sample](https://img.shields.io/badge/Flyover-Sample-blue)](https://developers.arcgis.com/ios/swift/sample-code/explore-scenes-in-flyover-ar/) 4 | 5 | Augmented reality experiences are designed to "augment" the physical world with virtual content that respects real world scale, position, and orientation of a device. In the case of Runtime, a SceneView displays 3D geographic data as virtual content on top of a camera feed which represents the real, physical world. 6 | 7 | The Augmented Reality (AR) toolkit component allows quick and easy integration of AR into your application for a wide variety of scenarios. The toolkit recognizes the following common patterns for AR:  8 | * **Flyover**: Flyover AR allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.  9 | * **Tabletop**: Scene content is anchored to a physical surface, as if it were a 3D-printed model.  10 | * **World-scale**: Scene content is rendered exactly where it would be in the physical world. A camera feed is shown and GIS content is rendered on top of that feed. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation. 11 | 12 | The AR toolkit component is comprised of one class: `ArcGISARView`. This is a subclass of `UIView` that contains the functionality needed to display an AR experience in your application. It uses `ARKit`, Apple's augmented reality framework to display the live camera feed and handle real world tracking and synchronization with the Runtime SDK's `AGSSceneView`. The `ArcGISARView` is responsible for starting and managing an `ARKit` session. It uses a user-provided `AGSLocationDataSource` for getting an initial GPS location and when continuous GPS tracking is required. 13 | 14 | ### Features of the AR component 15 | 16 | - Allows display of the live camera feed 17 | - Manages `ARKit` `ARSession` lifecycle 18 | - Tracks user location and device orientation through a combination of `ARKit` and the device GPS 19 | - Provides access to an `AGSSceneView` to display your GIS 3D data over the live camera feed 20 | - `ARScreenToLocation` method to convert a screen point to a real-world coordinate 21 | - Easy access to all `ARKit` and `AGSLocationDataSource` delegate methods 22 | 23 | ### Usage 24 | 25 | ```swift 26 | let arView = ArcGISARView(renderVideoFeed: true) 27 | view.addSubview(arView) 28 | arView.translatesAutoresizingMaskIntoConstraints = false 29 | NSLayoutConstraint.activate([ 30 | arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 31 | arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 32 | arView.topAnchor.constraint(equalTo: view.topAnchor), 33 | arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 34 | ]) 35 | 36 | 37 | // Create a simple scene. 38 | arView.sceneView.scene = AGSScene(basemapStyle: .arcGISImagery) 39 | 40 | // Set a AGSCLLocationDataSource, used to get our initial real-world location. 41 | arView.locationDataSource = AGSCLLocationDataSource() 42 | 43 | // Start tracking our location and device orientation 44 | arView.startTracking(.initial) { (error) in 45 | print("Start tracking error: \(String(describing: error))") 46 | } 47 | 48 | ``` 49 | 50 | You must also add the following entries to your application's `Info.plist` file. These are required to allow access to the camera (for the live video feed) and to allow access to location services (when using the `AGSCLLocationDataSource`): 51 | 52 | * Privacy – Camera Usage Description ([NSCameraUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription)) 53 | * Privacy – Location When In Use Usage Description ([NSLocationWhenInUseUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationwheninuseusagedescription)) 54 | 55 | To see it in action, try out the [Examples](../../Examples) and refer to [ARExample.swift](../../Examples/ArcGISToolkitExamples/ARExample.swift) in the project. 56 | -------------------------------------------------------------------------------- /Documentation/Bookmarks/README.md: -------------------------------------------------------------------------------- 1 | # Bookmarks 2 | 3 | The Bookmarks component will display a list of bookmarks in a table view and allows the user to select a bookmark and perform some action. 4 | 5 | ## Bookmarks Behavior 6 | 7 | The `BookmarksTableViewController` can be created using either an `AGSGeoView` or an array of `AGSBookmark`s. 8 | 9 | Clients must set the `delegate` property and implement the `bookmarksViewController:didSelect` delegate method in order to be notified when a bookmark is selected. 10 | 11 | The `BookmarksTableViewController` observes changes to the `map` or `scene` property on the `AGSGeoView` and also the map or scene's `bookmarks` property and will update the list of bookmarks accordingly. 12 | 13 | ## Usage 14 | 15 | ```swift 16 | bookmarksVC = BookmarksViewController(geoView: mapView) 17 | bookmarksVC?.delegate = self 18 | 19 | ... 20 | 21 | func bookmarksViewController(_ controller: BookmarksViewController, didSelect bookmark: AGSBookmark) { 22 | if let viewpoint = bookmark.viewpoint { 23 | mapView.setViewpoint(viewpoint, duration: 2.0) 24 | dismiss(animated: true) 25 | } 26 | } 27 | ``` 28 | 29 | To see it in action, try out the [Examples](../../Examples) and refer to [BookmarksExample.swift](../../Examples/ArcGISToolkitExamples/BookmarksExample.swift) in the project. 30 | -------------------------------------------------------------------------------- /Documentation/Compass/README.md: -------------------------------------------------------------------------------- 1 | # Compass 2 | 3 | The Compass (alias North arrow) shows where north is in the MapView. The Compass supports reset rotation. 4 | 5 | The ArcGIS Runtime SDK currently supports rotating the map with 2-finger gesture on MapView and SceneView interactively by default and while the map will snap to north when rotating using gestures, the compass provides an easier way. The Compass Toolkit component will appear when the map is rotated and, when tapped, re-orientates the map back to north and hides the compass icon (note that the MapView auto-snaps back to north when it's within a threshold of north, and in that case the compass also auto hides). 6 | 7 | ### Compass Behavior 8 | 9 | Whenever the map is not orientated North (non-zero bearing) the compass appears. When reset to north, it disappears. A property allows you to disable the auto-hide feature so that it always shows. 10 | 11 | When the compass is tapped, the map orients back to north (zero bearing), the default orientation and the compass fades away, or after a short duration disappears. 12 | 13 | ### Usage 14 | 15 | ```swift 16 | let compass = Compass(mapView: mapView) 17 | self.view.addSubview(compass) 18 | ``` 19 | 20 | To see it in action, try out the [Examples](../../Examples) and refer to [CompassExample.swift](../../Examples/ArcGISToolkitExamples/CompassExample.swift) in the project. 21 | -------------------------------------------------------------------------------- /Documentation/FloorFilter/README.md: -------------------------------------------------------------------------------- 1 | # FloorFilter 2 | 3 | The `FloorFilter` component simplifies visualization of GIS data for a specific floor of a building in your application. It allows you to filter down the floor plan data displayed in your geo view to a site, a building in the site, or a floor in the building. 4 | 5 | The ArcGIS Runtime SDK currently supports filtering a 2D floor aware map based on the sites, buildings, or levels in the map. 6 | 7 | 8 | 9 | ### Behavior 10 | 11 | When the Site button is tapped, a prompt opens so the user can select a site and then a facility. After selecting a site and facility, a list of levels is displayed either above or below the site button. 12 | 13 | ### Usage 14 | 15 | ```swift 16 | 17 | addChild(floorFilterViewController) 18 | view.addSubview(floorFilterViewController.view) 19 | floorFilterViewController.didMove(toParent: self) 20 | 21 | ``` 22 | 23 | To see it in action, try out the [Examples](../../Examples) and refer to [FloorFilterExample.swift](../../Examples/ArcGISToolkitExamples/FloorFilterExample.swift) in the project. 24 | -------------------------------------------------------------------------------- /Documentation/JobManager/README.md: -------------------------------------------------------------------------------- 1 | # Job Manager 2 | 3 | Jobs by definition are long running operations and especially when using mobile devices, applications are backgrounded, terminated and re-launched based on rules specific to each platform. Our jobs supports pause and resume workflows but we don't have an easy way to implement persistence to the application lifecycles without quite a bit custom code. 4 | 5 | The Job Manager is a toolkit component that you can just plug into the application and then give it tasks you want to persist when the application is backgrounded/terminated and an easy way to rehydrate when the application is re-launched. 6 | 7 | ### Usage 8 | 9 | ```swift 10 | // create and load a task 11 | let task = AGSGeodatabaseSyncTask(url: URL) 12 | task.load { error in 13 | 14 | // make sure we are still around... 15 | guard let strongSelf = self else { 16 | return 17 | } 18 | 19 | // make sure task does not get released 20 | guard let strongTask = task else { 21 | return 22 | } 23 | 24 | // set up params 25 | let params = AGSGenerateGeodatabaseParameters() 26 | // 27 | 28 | // generate job 29 | let job = strongTask.generateJob(with: params, downloadFileURL: downloadURL) 30 | 31 | // register the job with our JobManager shared instance 32 | JobManager.shared.register(job: job) 33 | 34 | // start the job 35 | job.start(statusHandler: strongSelf.jobStatusHandler, completion: strongSelf.jobCompletionHandler) 36 | 37 | } 38 | ``` 39 | 40 | To see it in action, try out the [Examples](../../Examples) and refer to [JobManagerExample.swift](../../Examples/ArcGISToolkitExamples/JobManagerExample.swift) in the project. 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Documentation/LegendViewController/README.md: -------------------------------------------------------------------------------- 1 | # Legend View Controller 2 | 3 | A Legend View Controller displays a legend for a set of layers in a Map or Scene contained in a MapView or SceneView. A legend conveys the meaning of the symbols used to represent features in the layer. For each layer, the legend contains a patch displaying the symbology used along with some explanatory text. The Legend View Controller is dynamic and only contains information about visible layers. As layers go in and out of scale range, or are turned on/off, the legend updates to include only those layers that are visible to the user in the MapView or SceneView. 4 | 5 | ### Usage 6 | 7 | ```swift 8 | let legendVC = LegendViewController.makeLegendViewController(geoView: mapView) 9 | self.present(legendVC, animated:true, completion:nil) 10 | ``` 11 | 12 | To see it in action, try out the [Examples](../../Examples) and refer to [LegendExample.swift](../../Examples/ArcGISToolkitExamples/LegendExample.swift) in the project. 13 | -------------------------------------------------------------------------------- /Documentation/MeasureToolbar/README.md: -------------------------------------------------------------------------------- 1 | # MeasureToolbar 2 | 3 | The MeasureToolbar provides an all-in-one toolbar that you can add to your application to give your user full measure capabilities. The user can sketch on the associated map view and get immediate feedback with geodetic measurements for length or area. 4 | 5 | ### Usage 6 | 7 | ```swift 8 | let measureToolbar = MeasureToolbar(mapView: mapView) 9 | view.addSubview(measureToolbar) 10 | ``` 11 | 12 | To see it in action, try out the [Examples](../../Examples) and refer to [MeasureExample.swift](../../Examples/ArcGISToolkitExamples/MeasureExample.swift) in the project. 13 | -------------------------------------------------------------------------------- /Documentation/PopupController/README.md: -------------------------------------------------------------------------------- 1 | # PopupController 2 | 3 | The PopupController makes it easy to wire up an `AGSPopupsViewController` for a complete feature editing and collecting experience. To use it, you instantiate the `PopupController` and hold on to it. You must pass it a reference to your `UIViewController` and to your `AGSGeoView`. 4 | 5 | ### Usage 6 | 7 | ```swift 8 | var popupController: PopupController? 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | // Create a map 14 | let portal = AGSPortal.arcGISOnline(withLoginRequired: false) 15 | let portalItem = AGSPortalItem(portal: portal, itemID: "<>")! 16 | map = AGSMap(item: portalItem) 17 | 18 | // set the map on the mapview 19 | mapView.map = map 20 | 21 | // instantiate the popup controller 22 | popupController = PopupController(geoViewController: self, geoView: mapView) 23 | } 24 | ``` 25 | 26 | Note: Make sure to add `Privacy - Photo Library Usage Description`, `Privacy - Microphone Usage Description`, and `Privacy - Camera Usage Description` to your project's `Info.plist` to correctly add attachments. 27 | 28 | To see it in action, try out the [Examples](../../Examples) and refer to [PopupExample.swift](../../Examples/ArcGISToolkitExamples/PopupExample.swift) in the project. 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | ### Table of Contents 2 | 3 | * [Augmented reality (AR)](AR) - Integrates the scene view with ARKit to enable augmented reality (AR). 4 | * [Bookmarks](Bookmarks) - Shows bookmarks, from a map, scene, or a list. 5 | * [Compass](Compass) - Shows a compass direction when the map is rotated. Auto-hides when the map points north up. 6 | * [FloorFilter](FloorFilter) - Allows to filter floor plan data in a geo view by a site, a building in the site, or a floor in the building. 7 | * [JobManager](JobManager) - Suspends and resumes ArcGIS Runtime tasks when the app is background, terminated, and relaunched. 8 | * [LegendViewController](LegendViewController) - Displays a legend for all the layers in a map or scene contained in an `AGSGeoView`. 9 | * [MeasureToolbar](MeasureToolbar) - Allows measurement of distances and areas on the map view. 10 | * [PopupController](PopupController) - Display details and media, edit attributes, geometry and related records, and manage the attachments of features and graphics (popups are defined in the popup property of features and graphics). 11 | * [Scalebar](Scalebar) - Displays current scale reference. 12 | * [TemplatePickerViewController](TemplatePicker) - Allows a user to choose a template from a list of `AGSFeatureTemplate` when creating new features. 13 | * [TimeSlider](TimeSlider) - Allows interactively defining a temporal range (i.e. time extent) and animating time moving forward or backward. Can be used to manipulate the time extent in a MapView or SceneView. 14 | -------------------------------------------------------------------------------- /Documentation/Scalebar/Images/alternating-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/Scalebar/Images/alternating-bar.png -------------------------------------------------------------------------------- /Documentation/Scalebar/Images/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/Scalebar/Images/bar.png -------------------------------------------------------------------------------- /Documentation/Scalebar/Images/dual-unit-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/Scalebar/Images/dual-unit-line.png -------------------------------------------------------------------------------- /Documentation/Scalebar/Images/graduated-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/Scalebar/Images/graduated-line.png -------------------------------------------------------------------------------- /Documentation/Scalebar/Images/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/Scalebar/Images/line.png -------------------------------------------------------------------------------- /Documentation/Scalebar/README.md: -------------------------------------------------------------------------------- 1 | # Scalebar 2 | 3 | A scalebar displays the representation of an accurate linear measurement on the map. It provides a visual indication through which users can determine the size of features or the distance between features on a map. A scale bar is a line or bar divided into parts. It is labeled with its ground length, usually in multiples of map units, such as tens of kilometers or hundreds of miles. 4 | 5 | The scalebar uses geodetic calculations to provide accurate measurements for maps of any spatial reference. The measurement is accurate for the center of the map extent being displayed. This means at smaller scales (zoomed way out) you might find it somewhat inaccurate at the extremes of the visible extent. As the map is panned and zoomed, the scalebar automatically grows and shrinks and updates its measurement based on the new map extent. 6 | 7 | ### Usage 8 | 9 | ```swift 10 | let scalebar = Scalebar(mapView: mapView) 11 | scalebar.style = .alternatingBar 12 | scalebar.units = .metric 13 | scalebar.alignment = .left 14 | view.addSubview(scalebar) 15 | ``` 16 | 17 | To see it in action, try out the [Examples](../../Examples) and refer to [ScalebarExample.swift](../../Examples/ArcGISToolkitExamples/ScalebarExample.swift) in the project. 18 | 19 | 20 | ### Styles 21 | 22 | | Style | Example | 23 | |------------- |-------- | 24 | |`line` |![line](Images/line.png) | 25 | |`graduated line` |![graduated line](Images/graduated-line.png) | 26 | |`bar` |![bar](Images/bar.png) | 27 | |`alternatingBar` |![alternating bar](Images/alternating-bar.png) | 28 | |`dualUnitLine` |![dual unit line](Images/dual-unit-line.png) | 29 | 30 | 31 | 32 | ### Units 33 | 34 | Two options are available - `metric` and `imperial`. Defaults to the option most appropriate for the device locale. `metric` displays distances in meters and kilometers depending on the map scale, and `imperial` displays distances in feet and miles. 35 | 36 | ### Alignment 37 | 38 | As you pan & zoom the map, the scalebar automatically grows and shrinks to update its measurements for the new map extent. The `alignment` property indicates how the scalebar has been placed on the UI and which section of the scalebar should remain fixed. For example, if you place it in the lower-left corner of the UI, set the alignment to `left` which means that it will grow/shrink on the right side. 39 | 40 | ### Customization 41 | 42 | You can customize many visual elements of the scalebar such as - 43 | 44 | * `fillColor` 45 | * `alternateFillColor` 46 | * `lineColor` 47 | * `shadowColor` 48 | * `textColor` 49 | * `textShadowColor` 50 | * `font` 51 | 52 | -------------------------------------------------------------------------------- /Documentation/TemplatePicker/README.md: -------------------------------------------------------------------------------- 1 | # TemplatePickerViewController 2 | 3 | The `TemplatePickerViewController` is a `UIViewController` subclass that allows the user to choose from a list of `AGSFeatureTemplates`. This is a common use case for creating a new feature. 4 | 5 | ### Usage 6 | 7 | ```swift 8 | 9 | // Instantiate the TemplatePickerViewController 10 | let templatePicker = TemplatePickerViewController(map: map) 11 | 12 | // Assign the delegate 13 | templatePicker.delegate = self 14 | 15 | // Present the template picker 16 | navigationController?.pushViewController(templatePicker, animated: true) 17 | ``` 18 | 19 | To see it in action, try out the [Examples](../../Examples) and refer to [TemplatePickerExample.swift](../../Examples/ArcGISToolkitExamples/TemplatePickerExample.swift) in the project. 20 | -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/black.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/blue.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/end-time-pinned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/end-time-pinned.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/labelmode-thumbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/labelmode-thumbs.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/labelmode-ticks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/labelmode-ticks.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/ocean-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/ocean-blue.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/only-playback-buttons-visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/only-playback-buttons-visible.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/only-slider-visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/only-slider-visible.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/start-time-pinned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/start-time-pinned.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/time-instant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/time-instant.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/time-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/time-window.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/Images/timeslider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Documentation/TimeSlider/Images/timeslider.png -------------------------------------------------------------------------------- /Documentation/TimeSlider/README.md: -------------------------------------------------------------------------------- 1 | # TimeSlider 2 | 3 | The Time Slider provides controls that allow you to visualize temporal data for time enabled layers in a map or scene. 4 | 5 | ### Features of the Time Slider 6 | 7 | ![timeslider](Images/timeslider.png) 8 | 9 | * Tap the **Play** button to play a time animation that steps through your data sequentially. Playback interval, direction and whether to repeat or reverse can be configured from the time slider properties. While playing, the **Pause** button displays in its place. 10 | * Tap the **Forward** button to step forward to the next time stamp. 11 | * Tap the **Back** button to step back to the previous time stamp. 12 | * Drag the **Lower Thumb** (Current Extent Start Time) or **Upper Thumb** (Current Extent End Time) to the right or left to step through your temporal data interactively. 13 | 14 | #### Slider Thumbs 15 | 16 | How the Time Slider's thumbs are presented will vary depending on whether the current time extent represents a time window or instant and whether the start or end time is pinned. 17 | 18 | * **Time Window**: If the current time extent represents a non-instantaneous duration of time, and neither the start nor end time is pinned, then two thumbs will be shown on the slider, as shown below. Both thumbs can be manipulated by end users. 19 | 20 | ![time-window](Images/time-window.png) 21 | 22 | * **Time Window - Start or End Time Pinned:** If the start or end time is pinned using the `isStartTimePinned` or `isEndTimePinned`, then the corresponding slider thumb is replaced with a bar to visually indicate that the start or end time cannot be manipulated. This allows user to cumulatively view all data from the pinned thumb until the specified position of other thumb. 23 | 24 | ![start-time-pinned](Images/start-time-pinned.png) 25 | 26 | ![end-time-pinned](Images/end-time-pinned.png) 27 | 28 | * **Time Instant:** When the Time Slider's current time extent is specified as a time instant (i.e. `AGSTimeExtent.startTime` equals `AGSTimeExtent.endTime`), then one slider thumb is shown to represent the specified instant. 29 | 30 | ![time-instant](Images/time-instant.png) 31 | 32 | #### Labels 33 | 34 | The labels are shown on slider to help user understand the time filtering options slider offers. 35 | 36 | * **Full Extent Labels:** Use `fullExtentLabelsVisible` to control the visibility of full extent labels. 37 | 38 | * **Current Extent Labels:** Use `LabelMode.thumbs`to display the labels for the current extent start and end time. 39 | 40 | ![labelmode-thumbs](Images/labelmode-thumbs.png) 41 | 42 | * **Time Step Interval Labels:** Use `LabelMode.ticks` to display the labels for the time step intervals instead of for the current time extent. 43 | 44 | ![labelmode-ticks](Images/labelmode-ticks.png) 45 | 46 | #### Display Components 47 | 48 | User can control whether to show `Slider` and/or `Playback Buttons` using `isSliderVisible` and `playbackButtonsVisible`. 49 | 50 | ![only-slider-visible](Images/only-slider-visible.png) 51 | 52 | ![only-playback-buttons-visible](Images/only-playback-buttons-visible.png) 53 | 54 | ### Usage 55 | 56 | Initialize TimeSlider and add constraints to position it. 57 | 58 | ```swift 59 | // Configure time slider 60 | let timeSlider = TimeSlider() 61 | timeSlider.labelMode = .ticks 62 | timeSlider.addTarget(self, action: #selector(TimeSliderExample.timeSliderValueChanged(timeSlider:)), for: .valueChanged) 63 | view.addSubview(timeSlider) 64 | 65 | // Add constraints to position the slider 66 | let margin: CGFloat = 10.0 67 | timeSlider.translatesAutoresizingMaskIntoConstraints = false 68 | timeSlider.bottomAnchor.constraint(equalTo: mapView.attributionTopAnchor, constant: -margin).isActive = true 69 | 70 | timeSlider.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: margin).isActive = true 71 | timeSlider.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -margin).isActive = true 72 | ``` 73 | 74 | Use one of three initialize helper function to setup properties of the Time Slider. 75 | 76 | ```swift 77 | public func initializeTimeProperties(geoView: AGSGeoView, observeGeoView: Bool, completion: @escaping (Error?)->Void) 78 | public func initializeTimeProperties(timeAwareLayer: AGSTimeAware, completion: @escaping (Error?)->Void) 79 | public func initializeTimeSteps(timeStepCount: Int, fullExtent: AGSTimeExtent, completion: @escaping (Error?)->Void) 80 | ``` 81 | 82 | To see it in action, try out the [Examples](../../Examples) and refer to [TimeSliderExample.swift](../../Examples/ArcGISToolkitExamples/TimeSliderExample.swift) in the project. 83 | 84 | ### Themes 85 | 86 | | Theme | Example | 87 | |:-----------: |:------: | 88 | |`Black` |![Black](Images/black.png) | 89 | |`Blue` |![Blue](Images/blue.png) | 90 | |`Ocean Blue` |![Ocean Blue](Images/ocean-blue.png) | 91 | 92 | ### Customization 93 | 94 | You can customize many visual elements of the TimeSlider such as - 95 | 96 | * `currentExtentFillColor` 97 | * `currentExtentLabelColor` 98 | * `currentExtentLabelFont` 99 | * `currentExtentLabelDateStyle` 100 | * `fullExtentBorderColor` 101 | * `fullExtentBorderWidth` 102 | * `fullExtentFillColor` 103 | * `fullExtentLabelColor` 104 | * `fullExtentLabelFont` 105 | * `fullExtentLabelDateStyle` 106 | * `timeStepIntervalLabelDateStyle` 107 | * `timeStepIntervalLabelColor` 108 | * `timeStepIntervalLabelFont` 109 | * `timeStepIntervalTickColor` 110 | * `thumbFillColor` 111 | * `thumbBorderColor` 112 | * `thumbBorderWidth` 113 | * `thumbSize` 114 | * `thumbCornerRadius` 115 | * `playbackButtonsFillColor` 116 | * `layerExtentFillColor` 117 | * `trackHeight` 118 | * `theme` 119 | -------------------------------------------------------------------------------- /Examples/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | ../.swiftlint.yml -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGIS 16 | import ArcGISToolkit 17 | 18 | @UIApplicationMain 19 | class AppDelegate: UIResponder, UIApplicationDelegate { 20 | var window: UIWindow? 21 | 22 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 23 | // Override point for customization after application launch. 24 | 25 | #warning("Require user to sign in with an ArcGIS identity or set your developer API key") 26 | /* 27 | Use of Esri location services, including basemaps and geocoding, requires either an ArcGIS identity or an API Key. For more information see https://links.esri.com/arcgis-runtime-security-auth. 28 | 1) ArcGIS identity: An ArcGIS named user account that is a member of an organization in ArcGIS Online or ArcGIS Enterprise. 29 | 2) API key: A permanent key that gives your application access to Esri location services. Create a new API key or access existing API keys from your ArcGIS for Developers dashboard (https://links.esri.com/arcgis-api-keys). 30 | Production deployment of applications built with ArcGIS Runtime requires you to license ArcGIS Runtime functionality. For more information see https://links.esri.com/arcgis-runtime-license-and-deploy. 31 | */ 32 | // Uncomment the following line to access Esri location services using an API key. 33 | // AGSArcGISRuntimeEnvironment.apiKey = "<#API Key#>" 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(_ application: UIApplication) { 38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 39 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 40 | } 41 | 42 | func applicationDidEnterBackground(_ application: UIApplication) { 43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(_ application: UIApplication) { 48 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 49 | } 50 | 51 | func applicationDidBecomeActive(_ application: UIApplication) { 52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 53 | } 54 | 55 | func applicationWillTerminate(_ application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | 59 | /// We need to forward these calls to ArcGIS so that we can be a good citizen and allow ArcGIS to call the completionHandler 60 | /// once the associated download session is finished handling events. 61 | func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { 62 | AGSApplicationDelegate.shared().application( 63 | application, 64 | handleEventsForBackgroundURLSession: identifier, 65 | completionHandler: completionHandler 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/BookmarksExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGISToolkit 17 | import ArcGIS 18 | 19 | class BookmarksExample: MapViewController, BookmarksViewControllerDelegate { 20 | var bookmarksVC: BookmarksViewController? 21 | var bookmarksButton = UIBarButtonItem() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Add Bookmark button that will display the BookmarksTableViewController. 27 | bookmarksButton = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: self, action: #selector(showBookmarks)) 28 | navigationItem.rightBarButtonItem = bookmarksButton 29 | 30 | // Create the map from a portal item and assign to the mapView. 31 | let portal = AGSPortal.arcGISOnline(withLoginRequired: false) 32 | let portalItem = AGSPortalItem(portal: portal, itemID: "16f1b8ba37b44dc3884afc8d5f454dd2") 33 | mapView.map = AGSMap(item: portalItem) 34 | 35 | // Create the BookmarksTableViewController. 36 | bookmarksVC = BookmarksViewController(geoView: mapView) 37 | 38 | // Add a cancel button. 39 | let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) 40 | bookmarksVC?.navigationItem.rightBarButtonItem = cancelButton 41 | bookmarksVC?.delegate = self 42 | } 43 | 44 | @objc 45 | func showBookmarks() { 46 | if let bookmarksVC = bookmarksVC { 47 | // Display the bookmarksVC as a popover controller. 48 | bookmarksVC.modalPresentationStyle = .popover 49 | if let popoverPresentationController = bookmarksVC.popoverPresentationController { 50 | popoverPresentationController.delegate = self 51 | popoverPresentationController.barButtonItem = bookmarksButton 52 | } 53 | present(bookmarksVC, animated: true) 54 | } 55 | } 56 | 57 | @objc 58 | func cancel() { 59 | dismiss(animated: true) 60 | } 61 | 62 | func bookmarksViewController(_ controller: BookmarksViewController, didSelect bookmark: AGSBookmark) { 63 | if let viewpoint = bookmark.viewpoint { 64 | mapView.setViewpoint(viewpoint, duration: 2.0) 65 | dismiss(animated: true) 66 | } 67 | } 68 | } 69 | 70 | extension BookmarksExample: UIPopoverPresentationControllerDelegate { 71 | func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? { 72 | return UINavigationController(rootViewController: controller.presentedViewController) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/CompassExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class CompassExample: MapViewController { 19 | var map: AGSMap? 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // Initialize an AGSMap with the Modern Antique basemap and center it on South America. 25 | map = AGSMap(basemapStyle: .arcGISModernAntique) 26 | map?.initialViewpoint = AGSViewpoint(latitude: -25, longitude: -56, scale: 6e7) 27 | mapView.map = map 28 | 29 | // Create the compass and add it to our view. 30 | let compass = Compass(mapView: mapView) 31 | compass.translatesAutoresizingMaskIntoConstraints = false 32 | self.view.addSubview(compass) 33 | 34 | // Get the superview's layout. 35 | let margins = view.layoutMarginsGuide 36 | 37 | // Position the compass in the top right corner using auto layout constraints. 38 | // The Compass handles width/height constraints, so we don't need to do that here. 39 | compass.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12.0).isActive = true 40 | compass.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/ExamplesViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | 17 | class ExamplesViewController: VCListViewController { 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | self.title = "Toolkit Samples" 22 | 23 | self.viewControllerInfos = [ 24 | ("Compass", CompassExample.self, nil), 25 | ("Measure", MeasureExample.self, nil), 26 | ("Scalebar", ScalebarExample.self, nil), 27 | ("Legend", LegendExample.self, nil), 28 | ("Job Manager", JobManagerExample.self, nil), 29 | ("Time Slider", TimeSliderExample.self, nil), 30 | ("Popup Controller", PopupExample.self, nil), 31 | ("Template Picker", TemplatePickerExample.self, nil), 32 | ("AR", ARExample.self, nil), 33 | ("Bookmarks", BookmarksExample.self, nil), 34 | ("Floor Filter", FloorFilterExample.self, nil) 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/FloorFilterExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import UIKit 17 | import ArcGIS 18 | import ArcGISToolkit 19 | 20 | class FloorFilterExample: MapViewController { 21 | var floorFilterVC: FloorFilterViewController? 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Create the map from a portal item and assign to the mapView. 27 | let portal = AGSPortal(url: URL(string: "https://indoors.maps.arcgis.com/")!, loginRequired: false) 28 | let portalItem = AGSPortalItem(portal: portal, itemID: "f133a698536f44c8884ad81f80b6cfc7") 29 | let map = AGSMap(item: portalItem) 30 | mapView.map = map 31 | 32 | // The expansion direction of the floor filter determines which way it will open. 33 | // If the floor filter is placed on the top of the screen, it is recommended to use the style 'down'. 34 | // If floor filter placed on the bottom of the screen, then use 'up'. 35 | let floorFilterVC = FloorFilterViewController.makeFloorFilterView( 36 | geoView: mapView, 37 | expansionDirection: .up 38 | ) 39 | self.floorFilterVC = floorFilterVC 40 | floorFilterVC.onSelectedLevelChangedListener = { 41 | print("Level was changed.") 42 | } 43 | 44 | // Add floor filter to the current view. 45 | floorFilterVC.view.translatesAutoresizingMaskIntoConstraints = false 46 | addChild(floorFilterVC) 47 | view.addSubview(floorFilterVC.view) 48 | floorFilterVC.didMove(toParent: self) 49 | 50 | let leadingConstraint = CGFloat(40) 51 | floorFilterVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leadingConstraint).isActive = true 52 | 53 | // This places the floor filter at the bottom of the view, 54 | // anchored to the top of the map view's attribution bar. 55 | floorFilterVC.view.bottomAnchor.constraint(equalTo: mapView.attributionTopAnchor, constant: -leadingConstraint).isActive = true 56 | 57 | // This places the floor filter at the top of the view, 58 | // just below the top anchor of the safe area layout guide. 59 | // Set the constraint to true and set the bottom constraint to false 60 | floorFilterVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: leadingConstraint).isActive = false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | NSCameraUsageDescription 29 | For collecting attachments in popups 30 | NSLocationWhenInUseUsageDescription 31 | For showing the current location in a map 32 | NSMicrophoneUsageDescription 33 | For collecting attachments in popups 34 | NSPhotoLibraryUsageDescription 35 | For collecting attachments in popups 36 | UIBackgroundModes 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/LegendExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class LegendExample: MapViewController { 19 | let portal = AGSPortal.arcGISOnline(withLoginRequired: false) 20 | var portalItem: AGSPortalItem? 21 | var map: AGSMap? 22 | var legendVC: LegendViewController? 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | // create and set map on mapView 28 | portalItem = AGSPortalItem(portal: portal, itemID: "1966ef409a344d089b001df85332608f") 29 | map = AGSMap(item: portalItem!) 30 | mapView.map = map 31 | 32 | legendVC = LegendViewController.makeLegendViewController(geoView: mapView) 33 | 34 | // add button that will show the SwitchBasemapViewController 35 | let bbi = UIBarButtonItem(title: "Legend", style: .plain, target: self, action: #selector(showLegendAction)) 36 | navigationItem.rightBarButtonItem = bbi 37 | } 38 | 39 | @objc 40 | func showLegendAction() { 41 | if let legendVC = legendVC { 42 | navigationController?.pushViewController(legendVC, animated: true) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/MeasureExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class MeasureExample: MapViewController { 19 | var measureToolbar: MeasureToolbar! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // set a map on our mapView 25 | let portalItem = AGSPortalItem(portal: AGSPortal.arcGISOnline(withLoginRequired: false), itemID: "32eb38c48f91421a96e64fe1af492030") 26 | let map = AGSMap(item: portalItem) 27 | mapView.map = map 28 | 29 | // create a MeasureToolbar and add it to the view controller 30 | measureToolbar = MeasureToolbar(mapView: mapView) 31 | measureToolbar.translatesAutoresizingMaskIntoConstraints = false 32 | view.addSubview(measureToolbar) 33 | measureToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 34 | measureToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 35 | measureToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true 36 | measureToolbar.heightAnchor.constraint(equalToConstant: 44).isActive = true 37 | } 38 | 39 | override func viewDidLayoutSubviews() { 40 | super.viewDidLayoutSubviews() 41 | 42 | // update content inset for mapview 43 | mapView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: measureToolbar.frame.height, right: 0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Misc/ARStatusViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ARKit 16 | import ArcGIS 17 | 18 | extension ARCamera.TrackingState { 19 | var description: String { 20 | switch self { 21 | case .normal: 22 | return "Normal" 23 | case .notAvailable: 24 | return "Tracking unavailable" 25 | case .limited(.excessiveMotion): 26 | return "Limited - Excessive Motion" 27 | case .limited(.insufficientFeatures): 28 | return "Limited - Insufficient Features" 29 | case .limited(.initializing): 30 | return "Limited - Initializing" 31 | default: 32 | return "" 33 | } 34 | } 35 | } 36 | 37 | extension AGSLocationDataSourceStatus { 38 | var description: String { 39 | switch self { 40 | case .stopped: 41 | return "Stopped" 42 | case .starting: 43 | return "Starting" 44 | case .started: 45 | return "Started" 46 | case .failedToStart: 47 | return "Failed to start" 48 | case .stopping: 49 | return "Stopping" 50 | @unknown default: 51 | fatalError("Unknown AGSLocationDataSourceStatus") 52 | } 53 | } 54 | } 55 | 56 | /// A view controller for display AR-related status information. 57 | class ARStatusViewController: UITableViewController { 58 | @IBOutlet var trackingStateLabel: UILabel! 59 | @IBOutlet var frameRateLabel: UILabel! 60 | @IBOutlet var errorDescriptionLabel: UILabel! 61 | @IBOutlet var sceneLabel: UILabel! 62 | @IBOutlet var translationFactorLabel: UILabel! 63 | @IBOutlet var horizontalAccuracyLabel: UILabel! 64 | @IBOutlet var verticalAccuracyLabel: UILabel! 65 | @IBOutlet var locationDataSourceStatusLabel: UILabel! 66 | 67 | /// The `ARKit` camera tracking state. 68 | var trackingState: ARCamera.TrackingState = .notAvailable { 69 | didSet { 70 | DispatchQueue.main.async { [weak self] in 71 | guard let self = self else { return } 72 | self.trackingStateLabel?.text = self.trackingState.description 73 | } 74 | } 75 | } 76 | 77 | /// The calculated frame rate of the `SceneView` and `ARKit` display. 78 | var frameRate: Int = 0 { 79 | didSet { 80 | DispatchQueue.main.async { [weak self] in 81 | guard let self = self else { return } 82 | self.frameRateLabel?.text = "\(self.frameRate) fps" 83 | } 84 | } 85 | } 86 | 87 | /// The current error message. 88 | var errorMessage: String? { 89 | didSet { 90 | DispatchQueue.main.async { [weak self] in 91 | guard let self = self else { return } 92 | self.errorDescriptionLabel?.text = self.errorMessage 93 | } 94 | } 95 | } 96 | 97 | /// The label for the currently selected scene. 98 | var currentScene: String = "None" { 99 | didSet { 100 | DispatchQueue.main.async { [weak self] in 101 | guard let self = self else { return } 102 | self.sceneLabel?.text = self.currentScene 103 | } 104 | } 105 | } 106 | 107 | /// The translation factor applied to the current scene. 108 | var translationFactor: Double = 1.0 { 109 | didSet { 110 | DispatchQueue.main.async { [weak self] in 111 | guard let self = self else { return } 112 | self.translationFactorLabel?.text = String(format: "%.1f", self.translationFactor) 113 | } 114 | } 115 | } 116 | 117 | /// The horizontal accuracy of the last location. 118 | var horizontalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { 119 | didSet { 120 | DispatchQueue.main.async { [weak self] in 121 | guard let self = self else { return } 122 | self.horizontalAccuracyLabel?.text = self.measurementFormatter.string(from: self.horizontalAccuracyMeasurement) 123 | } 124 | } 125 | } 126 | 127 | /// The vertical accuracy of the last location. 128 | var verticalAccuracyMeasurement = Measurement(value: 1, unit: UnitLength.meters) { 129 | didSet { 130 | DispatchQueue.main.async { [weak self] in 131 | guard let self = self else { return } 132 | self.verticalAccuracyLabel?.text = self.measurementFormatter.string(from: self.verticalAccuracyMeasurement) 133 | } 134 | } 135 | } 136 | 137 | /// The status of the location data source. 138 | var locationDataSourceStatus: AGSLocationDataSourceStatus = .stopped { 139 | didSet { 140 | DispatchQueue.main.async { [weak self] in 141 | guard let self = self else { return } 142 | self.locationDataSourceStatusLabel?.text = self.locationDataSourceStatus.description 143 | } 144 | } 145 | } 146 | 147 | private let measurementFormatter: MeasurementFormatter = { 148 | let formatter = MeasurementFormatter() 149 | formatter.unitOptions = [.naturalScale, .providedUnit] 150 | return formatter 151 | }() 152 | 153 | override func viewDidLoad() { 154 | super.viewDidLoad() 155 | 156 | // Add a blur effect behind the table view. 157 | tableView.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Misc/CalibrationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGIS 17 | import ArcGISToolkit 18 | 19 | /// A view displaying controls for adjusting a scene view's location, heading, and elevation. Used to calibrate an AR session. 20 | class CalibrationView: UIView { 21 | /// Denotes whether to show the elevation control and label; defaults to `true`. 22 | var elevationControlVisibility = true { 23 | didSet { 24 | elevationSlider.isHidden = !elevationControlVisibility 25 | elevationLabel.isHidden = !elevationControlVisibility 26 | } 27 | } 28 | 29 | /// The `ArcGISARView` containing the origin camera we will be updating. 30 | private var arcgisARView: ArcGISARView! 31 | 32 | /// The label displaying calibration directions. 33 | private let calibrationDirectionsLabel: UILabel = { 34 | let label = UILabel(frame: .zero) 35 | label.textAlignment = .center 36 | label.font = UIFont.systemFont(ofSize: 24.0) 37 | label.textColor = .darkText 38 | label.numberOfLines = 0 39 | label.text = "Calibrating..." 40 | return label 41 | }() 42 | 43 | /// The UISlider used to adjust elevation. 44 | private let elevationSlider: UISlider = { 45 | let slider = UISlider(frame: .zero) 46 | slider.minimumValue = -50.0 47 | slider.maximumValue = 50.0 48 | return slider 49 | }() 50 | 51 | /// The UISlider used to adjust heading. 52 | private let headingSlider: UISlider = { 53 | let slider = UISlider(frame: .zero) 54 | slider.minimumValue = -10.0 55 | slider.maximumValue = 10.0 56 | return slider 57 | }() 58 | 59 | /// The elevation label.. 60 | private let elevationLabel = UILabel(frame: .zero) 61 | 62 | /// Initialized a new calibration view with the `ArcGISARView`. 63 | /// 64 | /// - Parameters: 65 | /// - arcgisARView: The `ArcGISARView` containing the originCamera we're updating. 66 | init(_ arcgisARView: ArcGISARView) { 67 | self.arcgisARView = arcgisARView 68 | 69 | super.init(frame: .zero) 70 | 71 | // Create visual effects view to show the label on a blurred background. 72 | let labelView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 73 | labelView.layer.cornerRadius = 8.0 74 | labelView.layer.masksToBounds = true 75 | 76 | // Add the label to our label view and set up constraints. 77 | labelView.contentView.addSubview(calibrationDirectionsLabel) 78 | calibrationDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false 79 | NSLayoutConstraint.activate([ 80 | calibrationDirectionsLabel.leadingAnchor.constraint(equalTo: labelView.leadingAnchor, constant: 8), 81 | calibrationDirectionsLabel.trailingAnchor.constraint(equalTo: labelView.trailingAnchor, constant: -8), 82 | calibrationDirectionsLabel.topAnchor.constraint(equalTo: labelView.topAnchor, constant: 8), 83 | calibrationDirectionsLabel.bottomAnchor.constraint(equalTo: labelView.bottomAnchor, constant: -8) 84 | ]) 85 | 86 | // Add the label view to our view and set up constraints. 87 | addSubview(labelView) 88 | labelView.translatesAutoresizingMaskIntoConstraints = false 89 | NSLayoutConstraint.activate([ 90 | labelView.centerXAnchor.constraint(equalTo: centerXAnchor), 91 | labelView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 8.0) 92 | ]) 93 | 94 | // Add the heading label and slider. 95 | let headingLabel = UILabel(frame: .zero) 96 | headingLabel.text = "Heading" 97 | headingLabel.textColor = .yellow 98 | addSubview(headingLabel) 99 | headingLabel.translatesAutoresizingMaskIntoConstraints = false 100 | NSLayoutConstraint.activate([ 101 | headingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), 102 | headingLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16) 103 | ]) 104 | 105 | addSubview(headingSlider) 106 | headingSlider.translatesAutoresizingMaskIntoConstraints = false 107 | NSLayoutConstraint.activate([ 108 | headingSlider.leadingAnchor.constraint(equalTo: headingLabel.trailingAnchor, constant: 16), 109 | headingSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), 110 | headingSlider.centerYAnchor.constraint(equalTo: headingLabel.centerYAnchor) 111 | ]) 112 | 113 | // Add the elevation label and slider. 114 | elevationLabel.text = "Elevation" 115 | elevationLabel.textColor = .yellow 116 | addSubview(elevationLabel) 117 | elevationLabel.translatesAutoresizingMaskIntoConstraints = false 118 | NSLayoutConstraint.activate([ 119 | elevationLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), 120 | elevationLabel.bottomAnchor.constraint(equalTo: headingLabel.topAnchor, constant: -24) 121 | ]) 122 | 123 | addSubview(elevationSlider) 124 | elevationSlider.translatesAutoresizingMaskIntoConstraints = false 125 | NSLayoutConstraint.activate([ 126 | elevationSlider.leadingAnchor.constraint(equalTo: elevationLabel.trailingAnchor, constant: 16), 127 | elevationSlider.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), 128 | elevationSlider.centerYAnchor.constraint(equalTo: elevationLabel.centerYAnchor) 129 | ]) 130 | 131 | // Setup actions for the two sliders. The sliders operate as "joysticks", where moving the slider thumb will start a timer 132 | // which rotates or elevates the current camera when the timer fires. The elevation and heading delta 133 | // values increase the further you move away from center. Moving and holding the thumb a little bit from center 134 | // will rotate/elevate just a little bit, but get progressively more the further from center the thumb is moved. 135 | headingSlider.addTarget(self, action: #selector(headingChanged(_:)), for: .valueChanged) 136 | headingSlider.addTarget(self, action: #selector(touchUpHeading(_:)), for: [.touchUpInside, .touchUpOutside]) 137 | 138 | elevationSlider.addTarget(self, action: #selector(elevationChanged(_:)), for: .valueChanged) 139 | elevationSlider.addTarget(self, action: #selector(touchUpElevation(_:)), for: [.touchUpInside, .touchUpOutside]) 140 | 141 | elevationSlider.isHidden = !elevationControlVisibility 142 | elevationLabel.isHidden = !elevationControlVisibility 143 | } 144 | 145 | required init?(coder aDecoder: NSCoder) { 146 | fatalError("init(coder:) has not been implemented") 147 | } 148 | 149 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 150 | // If the user tapped in the view (and not in the sliders), do not handle the event. 151 | // This allows the view below the calibration view to handle touch events. In this case, 152 | // that view is the SceneView. 153 | let hitView = super.hitTest(point, with: event) 154 | if hitView == self { 155 | return nil 156 | } else { 157 | return hitView 158 | } 159 | } 160 | 161 | // The timers for the "joystick" behavior. 162 | private var elevationTimer: Timer? 163 | private var headingTimer: Timer? 164 | 165 | /// Handle an elevation slider value-changed event. 166 | /// 167 | /// - Parameter sender: The slider tapped on. 168 | @objc 169 | func elevationChanged(_ sender: UISlider) { 170 | if elevationTimer == nil { 171 | // Create a timer which elevates the camera when fired. 172 | elevationTimer = Timer(timeInterval: 0.25, repeats: true) { [weak self] (_) in 173 | let delta = self?.joystickElevation() ?? 0.0 174 | // print("elevate delta = \(delta)") 175 | self?.elevate(delta) 176 | } 177 | 178 | // Add the timer to the main run loop. 179 | guard let timer = elevationTimer else { return } 180 | RunLoop.main.add(timer, forMode: .default) 181 | } 182 | } 183 | 184 | /// Handle an heading slider value-changed event. 185 | /// 186 | /// - Parameter sender: The slider tapped on. 187 | @objc 188 | func headingChanged(_ sender: UISlider) { 189 | if headingTimer == nil { 190 | // Create a timer which rotates the camera when fired. 191 | headingTimer = Timer(timeInterval: 0.1, repeats: true) { [weak self] (_) in 192 | let delta = self?.joystickHeading() ?? 0.0 193 | // print("rotate delta = \(delta)") 194 | self?.rotate(delta) 195 | } 196 | 197 | // Add the timer to the main run loop. 198 | guard let timer = headingTimer else { return } 199 | RunLoop.main.add(timer, forMode: .default) 200 | } 201 | } 202 | 203 | /// Handle an elevation slider touchUp event. This will stop the timer. 204 | /// 205 | /// - Parameter sender: The slider tapped on. 206 | @objc 207 | func touchUpElevation(_ sender: UISlider) { 208 | elevationTimer?.invalidate() 209 | elevationTimer = nil 210 | sender.value = 0.0 211 | } 212 | 213 | /// Handle a heading slider touchUp event. This will stop the timer. 214 | /// 215 | /// - Parameter sender: The slider tapped on. 216 | @objc 217 | func touchUpHeading(_ sender: UISlider) { 218 | headingTimer?.invalidate() 219 | headingTimer = nil 220 | sender.value = 0.0 221 | } 222 | 223 | /// Rotates the camera by `deltaHeading`. 224 | /// 225 | /// - Parameter deltaHeading: The amount to rotate the camera. 226 | private func rotate(_ deltaHeading: Double) { 227 | let camera = arcgisARView.originCamera 228 | let newHeading = camera.heading + deltaHeading 229 | arcgisARView.originCamera = camera.rotate(toHeading: newHeading, pitch: camera.pitch, roll: camera.roll) 230 | } 231 | 232 | /// Change the cameras altitude by `deltaAltitude`. 233 | /// 234 | /// - Parameter deltaAltitude: The amount to elevate the camera. 235 | private func elevate(_ deltaAltitude: Double) { 236 | arcgisARView.originCamera = arcgisARView.originCamera.elevate(withDeltaAltitude: deltaAltitude) 237 | } 238 | 239 | /// Calculates the elevation delta amount based on the elevation slider value. 240 | /// 241 | /// - Returns: The elevation delta. 242 | private func joystickElevation() -> Double { 243 | let deltaElevation = Double(elevationSlider.value) 244 | return pow(deltaElevation, 2) / 50.0 * (deltaElevation < 0 ? -1.0 : 1.0) 245 | } 246 | 247 | /// Calculates the heading delta amount based on the heading slider value. 248 | /// 249 | /// - Returns: The heading delta. 250 | private func joystickHeading() -> Double { 251 | let deltaHeading = Double(headingSlider.value) 252 | return pow(deltaHeading, 2) / 25.0 * (deltaHeading < 0 ? -1.0 : 1.0) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Misc/Plane.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import ARKit 16 | 17 | /// Helper class to visualize a plane found by ARKit 18 | class Plane: SCNNode { 19 | let node: SCNNode 20 | 21 | init(anchor: ARPlaneAnchor, in sceneView: ARSCNView) { 22 | // Create a node to visualize the plane's bounding rectangle. 23 | let extent = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) 24 | node = SCNNode(geometry: extent) 25 | node.simdPosition = anchor.center 26 | 27 | // `SCNPlane` is vertically oriented in its local coordinate space, so 28 | // rotate it to match the orientation of `ARPlaneAnchor`. 29 | node.eulerAngles.x = -.pi / 2 30 | 31 | super.init() 32 | 33 | node.opacity = 0.6 34 | guard let material = node.geometry?.firstMaterial 35 | else { fatalError("SCNPlane always has one material") } 36 | 37 | material.diffuse.contents = UIColor.white 38 | 39 | // Add the plane node as child node so they appear in the scene. 40 | addChildNode(node) 41 | } 42 | 43 | required init?(coder aDecoder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/Misc/UserDirectionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | 17 | /// A custom view for displaying directions to the user. 18 | class UserDirectionsView: UIVisualEffectView { 19 | private let userDirectionsLabel: UILabel = { 20 | let label = UILabel(frame: .zero) 21 | label.textAlignment = .center 22 | label.font = UIFont.systemFont(ofSize: 24.0) 23 | label.textColor = .darkText 24 | label.numberOfLines = 0 25 | label.text = "Initializing ARKit..." 26 | return label 27 | }() 28 | 29 | override init(effect: UIVisualEffect?) { 30 | super.init(effect: effect) 31 | 32 | // Set a corner radius. 33 | layer.cornerRadius = 8.0 34 | layer.masksToBounds = true 35 | 36 | contentView.addSubview(userDirectionsLabel) 37 | userDirectionsLabel.translatesAutoresizingMaskIntoConstraints = false 38 | NSLayoutConstraint.activate([ 39 | userDirectionsLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), 40 | userDirectionsLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), 41 | userDirectionsLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 42 | userDirectionsLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) 43 | ]) 44 | } 45 | 46 | required init?(coder aDecoder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | /// Updates the displayed user directions string. If `message` is nil or empty, this will hide the view. If `message` is not empty, it will display the view. 51 | /// 52 | /// - Parameter message: the new string to display. 53 | func updateUserDirections(_ message: String?) { 54 | UIView.animate(withDuration: 0.25) { [weak self] in 55 | self?.alpha = (message?.isEmpty ?? true) ? 0.0 : 1.0 56 | self?.userDirectionsLabel.text = message 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/PopupExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class PopupExample: MapViewController { 19 | var map: AGSMap? 20 | 21 | var popupController: PopupController? 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Create a map 27 | map = AGSMap(basemapStyle: .arcGISTopographic) 28 | let featureTable = AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/DamageAssessment/FeatureServer/0")!) 29 | let featureLayer = AGSFeatureLayer(featureTable: featureTable) 30 | map?.operationalLayers.add(featureLayer) 31 | 32 | // Here we give the feature layer a default popup definition. 33 | // We have to load it first to create a default popup definition. 34 | // If you create the map from a portal item, you can define the popup definition 35 | // in the webmap and avoid this step. 36 | featureLayer.load { _ in 37 | featureLayer.popupDefinition = AGSPopupDefinition(popupSource: featureLayer) 38 | } 39 | 40 | // Another way to create the map is with a portal item: 41 | // let portal = AGSPortal.arcGISOnline(withLoginRequired: false) 42 | // let portalItem = AGSPortalItem(portal: portal, itemID: "<>")! 43 | // map = AGSMap(item: portalItem) 44 | 45 | // set the map on the mapview 46 | mapView.map = map 47 | 48 | // Log if there is any error loading the map 49 | map?.load { error in 50 | if let error = error { 51 | print("error loading map: \(error)") 52 | } 53 | } 54 | 55 | // instantiate the popup controller 56 | popupController = PopupController(geoViewController: self, geoView: mapView) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/ScalebarExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class ScalebarExample: MapViewController, AGSGeoViewTouchDelegate { 19 | var map: AGSMap? 20 | var scalebar: Scalebar? 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | map = AGSMap(basemapStyle: .arcGISTopographic) 26 | mapView.map = map 27 | 28 | let width = CGFloat(175) 29 | let xMargin = CGFloat(10) 30 | let yMargin = CGFloat(10) 31 | 32 | // lower left scalebar 33 | let sb = Scalebar(mapView: mapView) 34 | sb.units = .metric 35 | sb.alignment = .left 36 | sb.style = .alternatingBar 37 | view.addSubview(sb) 38 | 39 | // add constraints so it's anchored to lower left corner 40 | sb.translatesAutoresizingMaskIntoConstraints = false 41 | sb.widthAnchor.constraint(equalToConstant: width).isActive = true 42 | sb.bottomAnchor.constraint(equalTo: mapView.attributionTopAnchor, constant: -yMargin).isActive = true 43 | sb.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: xMargin).isActive = true 44 | scalebar = sb 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/TemplatePickerExample.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | import ArcGIS 17 | 18 | class TemplatePickerExample: MapViewController { 19 | var map: AGSMap? 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // Create a map 25 | 26 | map = AGSMap(basemapStyle: .arcGISTopographic) 27 | let featureTable = AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/DamageAssessment/FeatureServer/0")!) 28 | let featureLayer = AGSFeatureLayer(featureTable: featureTable) 29 | map?.operationalLayers.add(featureLayer) 30 | 31 | // Here we give the feature layer a default popup definition. 32 | // We have to load it first to create a default popup definition. 33 | // If you create the map from a portal item, you can define the popup definition 34 | // in the webmap and avoid this step. 35 | featureLayer.load { _ in 36 | featureLayer.popupDefinition = AGSPopupDefinition(popupSource: featureLayer) 37 | } 38 | 39 | // set the map on the mapview 40 | mapView.map = map 41 | 42 | // Log if there is any error loading the map 43 | map?.load { error in 44 | if let error = error { 45 | print("error loading map: \(error)") 46 | } 47 | } 48 | 49 | // add bar button item for showing templates 50 | let bbi = UIBarButtonItem(title: "Templates", style: .plain, target: self, action: #selector(showTemplates)) 51 | navigationItem.rightBarButtonItem = bbi 52 | } 53 | 54 | @objc 55 | private func showTemplates() { 56 | guard let map = map else { return } 57 | 58 | // Instantiate the TemplatePickerViewController 59 | let templatePicker = TemplatePickerViewController(map: map) 60 | 61 | // Assign the delegate 62 | templatePicker.delegate = self 63 | 64 | // Present the template picker 65 | self.navigationController?.pushViewController(templatePicker, animated: true) 66 | } 67 | } 68 | 69 | extension TemplatePickerExample: TemplatePickerViewControllerDelegate { 70 | public func templatePickerViewControllerDidCancel(_ templatePickerViewController: TemplatePickerViewController) { 71 | // This is where you handle the user canceling the template picker 72 | 73 | // dismiss the template picker 74 | navigationController?.popToViewController(self, animated: true) 75 | 76 | let alert = UIAlertController(title: "TemplatePickerExample", message: "User cancelled", preferredStyle: .alert) 77 | let action = UIAlertAction(title: "OK", style: .default, handler: nil) 78 | alert.addAction(action) 79 | alert.preferredAction = action 80 | present(alert, animated: true) 81 | } 82 | 83 | public func templatePickerViewController(_ templatePickerViewController: TemplatePickerViewController, didSelect featureTemplateInfo: FeatureTemplateInfo) { 84 | // This is where you handle the user making a selection with the template picker 85 | 86 | // dismiss the template picker 87 | navigationController?.popToViewController(self, animated: true) 88 | 89 | let alert = UIAlertController(title: "TemplatePickerExample", message: "User selected \(featureTemplateInfo.featureTemplate.name)", preferredStyle: .alert) 90 | let action = UIAlertAction(title: "OK", style: .default, handler: nil) 91 | alert.addAction(action) 92 | alert.preferredAction = action 93 | present(alert, animated: true) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/TimeSliderExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2018 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGISToolkit 17 | import ArcGIS 18 | 19 | class TimeSliderExample: MapViewController { 20 | private var map = AGSMap(basemapStyle: .arcGISTopographic) 21 | private var timeSlider = TimeSlider() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Set a map with ESRI topographic basemap to mapView 27 | mapView.map = map 28 | 29 | // Configure time slider 30 | timeSlider.isHidden = true 31 | timeSlider.labelMode = .ticks 32 | timeSlider.addTarget(self, action: #selector(TimeSliderExample.timeSliderValueChanged(timeSlider:)), for: .valueChanged) 33 | view.addSubview(timeSlider) 34 | 35 | // 36 | // Add constraints to position the slider 37 | let margin: CGFloat = 10.0 38 | timeSlider.translatesAutoresizingMaskIntoConstraints = false 39 | timeSlider.bottomAnchor.constraint(equalTo: mapView.attributionTopAnchor, constant: -margin).isActive = true 40 | timeSlider.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: margin).isActive = true 41 | timeSlider.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -margin).isActive = true 42 | 43 | // Add layer 44 | let mapImageLayer = AGSArcGISMapImageLayer(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/911CallsHotspot/MapServer")!) 45 | mapView.map?.operationalLayers.add(mapImageLayer) 46 | mapImageLayer.load { [weak self] (error) in 47 | // Make sure self is around 48 | guard let self = self else { 49 | return 50 | } 51 | 52 | // If layer fails to load then 53 | // return with an error. 54 | guard error == nil else { 55 | self.showError(error!) 56 | return 57 | } 58 | 59 | // Zoom to full extent of layer 60 | if let fullExtent = mapImageLayer.fullExtent { 61 | self.mapView.setViewpoint(AGSViewpoint(targetExtent: fullExtent), completion: nil) 62 | } 63 | 64 | self.timeSlider.initializeTimeProperties(geoView: self.mapView, observeGeoView: true) { [weak self] (error) in 65 | // Make sure self is around 66 | guard let self = self else { 67 | return 68 | } 69 | 70 | // If time slider fails to init then 71 | // return with an error. 72 | guard error == nil else { 73 | self.showError(error!) 74 | return 75 | } 76 | 77 | // Show the time slider 78 | self.timeSlider.isHidden = false 79 | } 80 | } 81 | } 82 | 83 | @objc 84 | func timeSliderValueChanged(timeSlider: TimeSlider) { 85 | if mapView.timeExtent != timeSlider.currentExtent { 86 | mapView.timeExtent = timeSlider.currentExtent 87 | } 88 | } 89 | 90 | // MARK: - Show Error 91 | 92 | private func showError(_ error: Error) { 93 | let alertController = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert) 94 | alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) 95 | present(alertController, animated: true) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Examples/ArcGISToolkitExamples/VCListViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGISToolkit 16 | 17 | open class VCListViewController: UITableViewController { 18 | public var storyboardName: String? 19 | 20 | public var viewControllerInfos: [(vcName: String, viewControllerType: UIViewController.Type, nibName: String?)] = [] { 21 | didSet { 22 | self.tableView.reloadData() 23 | } 24 | } 25 | 26 | override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 27 | return viewControllerInfos.count 28 | } 29 | 30 | override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 31 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 32 | cell.textLabel?.text = viewControllerInfos[indexPath.row].vcName 33 | return cell 34 | } 35 | 36 | override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 37 | let t = viewControllerInfos[indexPath.row].viewControllerType 38 | let nibName = viewControllerInfos[indexPath.row].nibName 39 | var vcOpt: UIViewController? 40 | 41 | // first check storyboard 42 | if let storyboardName = self.storyboardName { 43 | let sb = UIStoryboard(name: storyboardName, bundle: nil) 44 | if let nibName = nibName { 45 | // this is how you can check to see if that identifier is in the nib, based on http://stackoverflow.com/a/34650505/1687195 46 | if let dictionary = sb.value(forKey: "identifierToNibNameMap") as? NSDictionary { 47 | if dictionary.value(forKey: nibName) != nil { 48 | vcOpt = sb.instantiateViewController(withIdentifier: nibName) 49 | } 50 | } 51 | } 52 | } 53 | 54 | if vcOpt == nil { 55 | vcOpt = t.init(nibName: nibName, bundle: nil) 56 | } 57 | 58 | if let vc = vcOpt { 59 | navigationController?.pushViewController(vc, animated: true) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | This project contains examples of how to use the toolkit components. 2 | 3 | ## Instructions 4 | 5 | 6 | 1. Clone or download this repo 7 | 2. Open the Examples project in Xcode by double clicking on the `ArcGISToolkitExamples.xcodeproj` file 8 | 3. Build and run the project 9 | -------------------------------------------------------------------------------- /Examples/exportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | /* 3 | COPYRIGHT 1995-2021 ESRI 4 | 5 | All rights reserved under the copyright laws of the United States 6 | and applicable international laws, treaties, and conventions. 7 | 8 | This material is licensed for use under the Esri Master License 9 | Agreement (MLA), and is bound by the terms of that agreement. 10 | You may redistribute and use this code without modification, 11 | provided you adhere to the terms of the MLA and include this 12 | copyright notice. 13 | 14 | See use restrictions at http://www.esri.com/legal/pdfs/mla_e204_e300/english 15 | 16 | For additional information, contact: 17 | Environmental Systems Research Institute, Inc. 18 | Attn: Contracts and Legal Services Department 19 | 380 New York Street 20 | Redlands, California, USA 92373 21 | 22 | email: contracts@esri.com 23 | */ 24 | 25 | import PackageDescription 26 | 27 | // This is setup by default to use a hosted version of the ArcGIS Runtime for iOS. 28 | // To use a local installation of ArcGIS Runtime for iOS, uncomment the lines below declaring 29 | // the `localArcGISPackage` constant and use that as the package dependency instead of the 30 | // hosted version. 31 | // import class Foundation.ProcessInfo 32 | // let localArcGISPackage = ProcessInfo.processInfo.environment["HOME"]! + "/Library/SDKs/ArcGIS" 33 | 34 | let package = Package( 35 | name: "arcgis-runtime-toolkit-ios", 36 | platforms: [ 37 | .iOS(.v14) 38 | ], 39 | products: [ 40 | .library( 41 | name: "ArcGISToolkit", 42 | targets: ["ArcGISToolkit"] 43 | ) 44 | ], 45 | dependencies: [ 46 | .package( 47 | name: "arcgis-runtime-ios", 48 | // path: localArcGISPackage 49 | url: "https://github.com/Esri/arcgis-runtime-ios", .upToNextMinor(from: "100.15.0") 50 | ) 51 | ], 52 | targets: [ 53 | .target( 54 | name: "ArcGISToolkit", 55 | dependencies: [.product(name: "ArcGIS", package: "arcgis-runtime-ios")], 56 | resources: [ 57 | .process("Resources/PrivacyInfo.xcprivacy") 58 | ] 59 | ), 60 | .testTarget( 61 | name: "ArcGISToolkitTests", 62 | dependencies: ["ArcGISToolkit"] 63 | ) 64 | ] 65 | ) 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcGIS Runtime Toolkit for iOS 2 | 3 | [![doc](https://img.shields.io/badge/Doc-purple)](Documentation) 4 | [![CocoaPods](https://img.shields.io/cocoapods/v/ArcGIS-Runtime-Toolkit-iOS)](https://cocoapods.org/) 5 | [![SPM](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager/) 6 | 7 | The ArcGIS Runtime SDK for iOS Toolkit contains components that will simplify your iOS app development. Check out the 8 | [Examples](/Examples) project to see these components in action or read through the [Documentation](/Documentation) to 9 | learn more about them. 10 | 11 | 12 | >Try [ArcGIS Maps SDK for Swift version 200](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit/) 13 | > 14 | >ArcGIS Runtime SDK for iOS version 100.15 is a long-term support release focused exclusively on bug fixes and minor updates. 15 | > 16 | >ArcGIS Maps SDK for Swift version 200 builds on the proven architecture of 100.15, and provides a new API designed exclusively for developing iOS apps using Swift - Apple's modern, powerful, and intuitive programming language. 17 | 18 | To use Toolkit in your project: 19 | 20 | * **[Install with Swift Package Manager](#swift-package-manager)** - Add 21 | `https://github.com/Esri/arcgis-runtime-toolkit-ios` as the package repository URL. 22 | * **[Install with CocoaPods](#cocoapods)** - Add `pod 'ArcGIS-Runtime-Toolkit-iOS'` to your podfile 23 | * **[Build manually](#manual)** - Build and include manually if you'd like to customize or extend toolkit 24 | 25 | ## Toolkit Components 26 | 27 | * **[Augmented reality (AR)](Documentation/AR)** - Integrates the scene view with ARKit to enable augmented reality 28 | (AR). 29 | * **[Bookmarks](Documentation/Bookmarks)** - Shows bookmarks, from a map, scene, or a list. 30 | * **[Compass](Documentation/Compass)** - Shows a compass direction when the map is rotated. Auto-hides when the map 31 | points north up. 32 | * **[FloorFilter](Documentation/FloorFilter)** - Allows to filter floor plan data in a geo view by a site, a building in the site, or a floor in the building. 33 | * **[JobManager](Documentation/JobManager)** - Suspends and resumes ArcGIS Runtime tasks when the app is background, 34 | terminated, and relaunched. 35 | * **[LegendViewController](Documentation/LegendViewController)** - Displays a legend for all the layers in a map or 36 | scene contained in an `AGSGeoView`. 37 | * **[MeasureToolbar](Documentation/MeasureToolbar)** - Allows measurement of distances and areas on the map view. 38 | * **[PopupController](Documentation/PopupController)** - Display details and media, edit attributes, geometry and 39 | related records, and manage the attachments of features and graphics (popups are defined in the popup property of 40 | features and graphics). 41 | * **[Scalebar](Documentation/Scalebar)** - Displays current scale reference. 42 | * **[TemplatePickerViewController](Documentation/TemplatePicker)** - Allows a user to choose a template from a list of 43 | `AGSFeatureTemplate` when creating new features. 44 | * **[TimeSlider](Documentation/TimeSlider)** - Allows interactively defining a temporal range (i.e. time extent) and 45 | animating time moving forward or backward. Can be used to manipulate the time extent in a MapView or SceneView. 46 | 47 | ## Requirements 48 | 49 | * [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/ios/) 100.15.0 (or higher) 50 | * Xcode 13.0 (or higher) 51 | 52 | The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *14.0*, meaning that it can run on devices with *iOS 53 | 14.0* or newer. 54 | 55 | ## Instructions 56 | 57 | ### Swift Package Manager 58 | 59 | 1. Open your project in Xcode 60 | 2. Go to *File* > *Swift Packages* > *Add Package Dependency* option 61 | 3. Enter `https://github.com/Esri/arcgis-runtime-toolkit-ios` as the package repository URL 62 | 4. Choose version 100.15.0 or a later version. Click Next. 63 | 64 | Note: The Toolkit Swift Package adds the ArcGIS SDK Swift Package as a dependency so no need to add both separately. If 65 | you already have the ArcGIS SDK Swift Package delete that and just add the Toolkit Swift Package. 66 | 67 | New to Swift Package Manager? Visit [swift.org/package-manager/](https://swift.org/package-manager/). 68 | 69 | ### Cocoapods 70 | 71 | 1. Add `pod 'ArcGIS-Runtime-Toolkit-iOS'` to your podfile 72 | 2. Run `pod install`. This will download the toolkit and the ArcGIS Runtime SDK for iOS which the toolkit depends upon 73 | and then configure your project to reference them both 74 | 3. Add `import ArcGISToolkit` in your source code and start using the toolkit components 75 | 76 | New to cocoapods? Visit [cocoapods.org](https://cocoapods.org/) 77 | 78 | ### Manual 79 | 80 | 1. Clone or download this repo 81 | 2. Drag and Drop the `arcgis-runtime-toolkit-ios` folder into your project through the Xcode Project Navigator pane 82 | 3. Add the *ArcGISToolkit* library in your app, by adding it to the Frameworks, Libraries, and Embedded Content section 83 | of the General pane for your app target. The *ArcGISToolkit* library contains the *ArcGIS Runtime SDK for iOS* 84 | library, so you don't need to add that separately. 85 | 4. Add `import ArcGIS` and `import ArcGISToolkit` in your source code and start using the toolkit components 86 | 87 | Note: The manual installation method also allows you to use a local installation ArcGIS Runtime SDK for iOS by making 88 | minor edits to the [swift package](Package.swift). 89 | 90 | ## Configure API Key 91 | 92 | Some of the toolkit components and examples utilize a set of ready-to-use ArcGIS Platform services, including basemaps, and therefore require an API Key to be set in `AppDelegate.swift`. Please see the [setup guide](https://developers.arcgis.com/ios/get-started/#3-access-services-and-content-with-an-api-key) for more information. 93 | 94 | ## SwiftLint 95 | 96 | Both the Toolkit and Examples app support SwiftLint. You can install SwiftLint from 97 | [here](https://github.com/realm/SwiftLint). It is not necessary to have it installed in order to build, but you will 98 | get a warning without it. The specific rules the linter uses can be found in the `swiftlint.yml` files in the `Toolkit` 99 | and `Examples` directories. 100 | 101 | ## Additional Resources 102 | 103 | * [Developers guide documentation](https://developers.arcgis.com/ios) 104 | * [Runtime API Reference](https://developers.arcgis.com/ios/api-reference) 105 | * [Samples](https://github.com/Esri/arcgis-runtime-samples-ios) 106 | * Got a question? Ask the community on our 107 | [forum](http://geonet.esri.com/community/developers/native-app-developers/arcgis-runtime-sdk-for-ios) 108 | 109 | ## Issues 110 | 111 | Find a bug or want to request a new feature? Please let us know by [submitting an 112 | issue](https://github.com/Esri/arcgis-runtime-toolkit-ios/issues/new). 113 | 114 | ## Contributing 115 | 116 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for 117 | contributing](https://github.com/esri/contributing). 118 | 119 | ## Licensing 120 | 121 | Copyright 2017 - 2022 Esri 122 | 123 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 124 | License. You may obtain a copy of the License at 125 | 126 | 127 | 128 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 129 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 130 | language governing permissions and limitations under the License. 131 | 132 | A copy of the license is available in the repository's [LICENSE]( /LICENSE) file. 133 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/BookmarksTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGIS 17 | 18 | class BookmarksTableViewController: UITableViewController { 19 | var bookmarks = [AGSBookmark]() { 20 | didSet { 21 | if let indexPath = tableView.indexPathForSelectedRow { 22 | tableView.deselectRow(at: indexPath, animated: false) 23 | } 24 | tableView.reloadData() 25 | } 26 | } 27 | 28 | weak var delegate: BookmarksViewControllerDelegate? 29 | 30 | private var cellReuseIdentifier = "cell" 31 | 32 | // Private property to store selection action for table cell. 33 | private var selectAction: ((AGSBookmark) -> Void)? 34 | 35 | // Executed for tableview row selection. 36 | func setSelectAction(_ action : @escaping ((AGSBookmark) -> Void)) { 37 | self.selectAction = action 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier) 43 | } 44 | 45 | // MARK: - Table view data source 46 | 47 | override func numberOfSections(in tableView: UITableView) -> Int { 48 | return 1 49 | } 50 | 51 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 52 | return bookmarks.count 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 56 | let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) 57 | cell.backgroundColor = .clear 58 | cell.textLabel?.text = bookmarks[indexPath.row].name 59 | return cell 60 | } 61 | 62 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 63 | selectAction?(bookmarks[indexPath.row]) 64 | tableView.deselectRow(at: indexPath, animated: true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/BookmarksViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGIS 17 | 18 | /// The protocol you implement to respond to user bookmark selections. 19 | /// - Since: 100.7.0 20 | public protocol BookmarksViewControllerDelegate: AnyObject { 21 | /// Tells the delegate that the user has selected a bookmark. 22 | /// 23 | /// - Parameters: 24 | /// - controller: The view controller calling the delegate method. 25 | /// - bookmark: The new bookmark selected. 26 | /// - Since: 100.7.0 27 | func bookmarksViewController(_ controller: BookmarksViewController, didSelect bookmark: AGSBookmark) 28 | } 29 | 30 | /// The `BookmarksViewController` will display a list of bookmarks in a table view and allows the user to select a bookmark and perform some action. 31 | /// It can be created using either an `AGSGeoView` or an array of `AGSBookmark`s. 32 | /// - Since: 100.7.0 33 | public class BookmarksViewController: UIViewController { 34 | /// The array of `AGSBookmark`s to display. 35 | /// - Since: 100.7.0 36 | public var bookmarks = [AGSBookmark]() { 37 | didSet { 38 | tableViewController.bookmarks = bookmarks 39 | } 40 | } 41 | 42 | /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the `AGSBookmark`s to display. 43 | /// - Since: 100.7.0 44 | public var geoView: AGSGeoView? { 45 | didSet { 46 | geoViewDidChange(oldValue) 47 | } 48 | } 49 | 50 | /// The delegate of the bookmarks view controller. Clients must set the `delegate` property and implement the `bookmarksViewController:didSelect` delegate method in order to be notified when a bookmark is selected. 51 | /// - Since: 100.7.0 52 | public weak var delegate: BookmarksViewControllerDelegate? 53 | 54 | /// The observations used to observe changes to the bookmarks and map/scene. 55 | private var bookmarksObservation: NSKeyValueObservation? 56 | private var mapOrSceneObservation: NSKeyValueObservation? 57 | 58 | /// The view controller which handles the actual bookmark display and user interaction. 59 | private lazy var tableViewController = BookmarksTableViewController() 60 | 61 | /// Returns a BookmarksViewController which will display the `AGSBookmark`s in the `bookmarks` array. 62 | /// - Parameter bookmarks: A sequence of `AGSBookmark`s to display. 63 | /// - Since: 100.7.0 64 | public init(bookmarks: S) where S.Element == AGSBookmark { 65 | super.init(nibName: nil, bundle: nil) 66 | self.bookmarks.append(contentsOf: bookmarks) 67 | tableViewController.bookmarks = self.bookmarks 68 | sharedInit() 69 | } 70 | 71 | /// Returns a BookmarksViewController which will display the array of `AGSBookmark` found in the `AGSGeoView`s `AGSMap` or `AGSScene`. 72 | /// - Parameter geoView: The `AGSGeoView` containing the `AGSBookmark` array in either the map or scene. 73 | /// - Since: 100.7.0 74 | public init(geoView: AGSGeoView) { 75 | super.init(nibName: nil, bundle: nil) 76 | self.geoView = geoView 77 | geoViewDidChange(nil) 78 | sharedInit() 79 | } 80 | 81 | @available(*, unavailable) 82 | required init?(coder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | private func sharedInit() { 87 | title = "Bookmarks" 88 | } 89 | 90 | override public func viewDidLoad() { 91 | super.viewDidLoad() 92 | 93 | // Setup our internal BookmarksTableViewController and add it as a child. 94 | addChild(tableViewController) 95 | view.addSubview(tableViewController.view) 96 | tableViewController.view.translatesAutoresizingMaskIntoConstraints = false 97 | NSLayoutConstraint.activate([ 98 | tableViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 99 | tableViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 100 | tableViewController.view.topAnchor.constraint(equalTo: view.topAnchor), 101 | tableViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) 102 | ]) 103 | tableViewController.didMove(toParent: self) 104 | 105 | // Set the closure to be executed when the user selects a bookmark. 106 | tableViewController.setSelectAction { [weak self] (bookmark: AGSBookmark) in 107 | guard let self = self, let delegate = self.delegate else { return } 108 | delegate.bookmarksViewController(self, didSelect: bookmark) 109 | } 110 | } 111 | 112 | private func geoViewDidChange(_ previousGeoView: AGSGeoView?) { 113 | if let mapView = geoView as? AGSMapView { 114 | mapView.map?.load { [weak self] (error) in 115 | guard let self = self, let mapView = self.geoView as? AGSMapView else { return } 116 | if let error = error { 117 | print("Error loading map: \(error)") 118 | } else { 119 | self.bookmarks = mapView.map?.bookmarks as? [AGSBookmark] ?? [] 120 | } 121 | } 122 | 123 | // Add an observer to handle changes to the map.bookmarks array. 124 | addBookmarksObserver(map: mapView.map) 125 | 126 | // Add an observer to handle changes to the mapView.map. 127 | mapOrSceneObservation = mapView.observe(\.map) { [weak self] (_, _) in 128 | self?.bookmarks = mapView.map?.bookmarks as? [AGSBookmark] ?? [] 129 | 130 | // When the map changes, we again need to add an observer to handle changes to the map.bookmarks array. 131 | self?.addBookmarksObserver(map: mapView.map) 132 | } 133 | } else if let sceneView = geoView as? AGSSceneView { 134 | sceneView.scene?.load { [weak self] (error) in 135 | guard let self = self, let sceneView = self.geoView as? AGSSceneView else { return } 136 | if let error = error { 137 | print("Error loading map: \(error)") 138 | } else { 139 | self.bookmarks = sceneView.scene?.bookmarks as? [AGSBookmark] ?? [] 140 | } 141 | } 142 | 143 | // Add an observer to handle changes to the scene.bookmarks array. 144 | addBookmarksObserver(scene: sceneView.scene) 145 | 146 | // Add an observer to handle changes to the sceneView.scene. 147 | mapOrSceneObservation = sceneView.observe(\.scene) { [weak self] (_, _) in 148 | self?.bookmarks = sceneView.scene?.bookmarks as? [AGSBookmark] ?? [] 149 | 150 | // When the scene changes, we again need to add an observer to handle changes to the scene.bookmarks array. 151 | self?.addBookmarksObserver(scene: sceneView.scene) 152 | } 153 | } 154 | } 155 | 156 | private func addBookmarksObserver(map: AGSMap?) { 157 | bookmarksObservation = map?.observe(\.bookmarks) { [weak self] (map, _) in 158 | DispatchQueue.main.async { 159 | self?.bookmarks = map.bookmarks as? [AGSBookmark] ?? [] 160 | } 161 | } 162 | } 163 | 164 | private func addBookmarksObserver(scene: AGSScene?) { 165 | bookmarksObservation = scene?.observe(\.bookmarks) { [weak self] (scene, _) in 166 | DispatchQueue.main.async { 167 | self?.bookmarks = scene.bookmarks as? [AGSBookmark] ?? [] 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/CancelGroup.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import ArcGIS 15 | import Foundation 16 | 17 | /** 18 | Wraps multiple AGSCancelables into a single cancelable object. 19 | */ 20 | @objc 21 | public class CancelGroup: NSObject, AGSCancelable { 22 | /// Cancels all the AGSCancelables in the group. 23 | public func cancel() { 24 | children.forEach { $0.cancel() } 25 | _canceled = true 26 | } 27 | 28 | private var _canceled: Bool = false 29 | 30 | /// Whether or not the group is canceled. 31 | public func isCanceled() -> Bool { 32 | return _canceled 33 | } 34 | 35 | /// The children associated with this group. 36 | public var children: [AGSCancelable] = [AGSCancelable]() 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Coalescer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import Foundation 15 | 16 | internal class Coalescer { 17 | // Class to coalesce actions into intervals. 18 | // This is helpful for the Scalebar because we get updates to the visibleArea up to 60hz and we 19 | // don't need to redraw the Scalebar that often 20 | 21 | var dispatchQueue: DispatchQueue 22 | var interval: DispatchTimeInterval 23 | var action: (() -> Void) 24 | 25 | init (dispatchQueue: DispatchQueue, interval: DispatchTimeInterval, action: @escaping (() -> Void)) { 26 | self.dispatchQueue = dispatchQueue 27 | self.interval = interval 28 | self.action = action 29 | } 30 | 31 | private var count = 0 32 | 33 | func ping() { 34 | // synchronize to a serial queue, in this case main thread 35 | if !Thread.isMainThread { 36 | DispatchQueue.main.async { self.ping() } 37 | return 38 | } 39 | 40 | // increment the count 41 | count += 1 42 | 43 | // the first time the count is incremented, it dispatches the action 44 | if count == 1 { 45 | dispatchQueue.asyncAfter(deadline: DispatchTime.now() + interval) { 46 | // call the action 47 | self.action() 48 | 49 | // reset the count 50 | self.resetCount() 51 | } 52 | } 53 | } 54 | 55 | private func resetCount() { 56 | // synchronize to a serial queue, in this case main thread 57 | if !Thread.isMainThread { 58 | DispatchQueue.main.async { self.count = 0 } 59 | } else { 60 | self.count = 0 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Compass.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGIS 16 | 17 | public class Compass: UIImageView { 18 | public var heading: Double = 0.0 { // Rotation - bound to MapView.MapRotation 19 | didSet { 20 | mapView.setViewpointRotation(heading, completion: nil) 21 | } 22 | } 23 | public var autoHide: Bool = true { // Auto hides when north is up 24 | didSet { 25 | animateCompass() 26 | } 27 | } 28 | public var width: CGFloat = 30.0 { 29 | didSet { 30 | widthConstraint?.isActive = false 31 | widthConstraint = widthAnchor.constraint(equalToConstant: width) 32 | widthConstraint?.isActive = true 33 | } 34 | } 35 | public var height: CGFloat = 30 { 36 | didSet { 37 | heightConstraint?.isActive = false 38 | heightConstraint = heightAnchor.constraint(equalToConstant: height) 39 | heightConstraint?.isActive = true 40 | } 41 | } 42 | 43 | private var mapView: AGSMapView 44 | 45 | // the width and height constraints 46 | private var widthConstraint: NSLayoutConstraint? 47 | private var heightConstraint: NSLayoutConstraint? 48 | 49 | private var rotationObservation: NSKeyValueObservation? 50 | 51 | public init(mapView: AGSMapView) { 52 | self.mapView = mapView 53 | 54 | super.init(frame: .zero) 55 | 56 | // Set our image to the CompassIcon in the Assets 57 | image = UIImage(named: "CompassIcon", in: .module, compatibleWith: nil) 58 | 59 | // add gesture recognizer to know when arrow is tapped 60 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(compassTapped)) 61 | self.addGestureRecognizer(tapGestureRecognizer) 62 | self.isUserInteractionEnabled = true 63 | 64 | // animate the compass visibility, if necessary 65 | animateCompass() 66 | 67 | // Add Compass as an observer of the mapView's rotation. 68 | rotationObservation = mapView.observe(\.rotation, options: .new) {[weak self] (_, change) in 69 | guard let rotation = change.newValue else { 70 | return 71 | } 72 | 73 | // make sure that UI changes are made on the main thread 74 | DispatchQueue.main.async { 75 | guard let self = self else { 76 | return 77 | } 78 | 79 | let mapRotation = self.degreesToRadians(degrees: (360 - rotation)) 80 | // Rotate north arrow to match the map view rotation. 81 | self.transform = CGAffineTransform(rotationAngle: mapRotation) 82 | // Animate the compass visibility (if necessary) 83 | self.animateCompass() 84 | } 85 | } 86 | } 87 | 88 | public required init?(coder aDecoder: NSCoder) { 89 | fatalError("init(coder:) has not been implemented") 90 | } 91 | 92 | @objc 93 | func compassTapped() { 94 | mapView.setViewpointRotation(0, completion: nil) 95 | } 96 | 97 | func animateCompass() { 98 | let alpha: CGFloat = (mapView.rotation == 0.0) && autoHide ? 0.0 : 1.0 99 | if alpha != self.alpha { 100 | UIView.animate(withDuration: 0.25) { 101 | self.alpha = alpha 102 | } 103 | } 104 | } 105 | 106 | func degreesToRadians(degrees: Double) -> CGFloat { 107 | return CGFloat(degrees * Double.pi / 180) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Extensions.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import Foundation 15 | import UIKit 16 | 17 | #if !SWIFT_PACKAGE 18 | // This is a workaround for cocoapods compatibility. 19 | extension Bundle { 20 | static var module: Bundle { Bundle(identifier: "org.cocoapods.ArcGISToolkit")! } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/FloorFilter/FloorFilterViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import UIKit 17 | import ArcGIS 18 | 19 | /// View Model class that contains the Data Model of the Floor Filter 20 | /// Also contains the business logic to filter and change the map extent based on selected site/level/facility 21 | final class FloorFilterViewModel { 22 | /// The MapView, Map and Floor Manager are set in the FloorFilterViewController when the map is loaded 23 | var mapView: AGSMapView? 24 | 25 | var floorManager: AGSFloorManager? { 26 | return mapView?.map?.floorManager 27 | } 28 | 29 | var sites: [AGSFloorSite] { 30 | return floorManager?.sites ?? [] 31 | } 32 | 33 | /// Facilities in the selected site 34 | /// If no site is selected then the list is empty 35 | /// If the sites data does not exist in the map, then use all the facilities in the map 36 | var facilities: [AGSFloorFacility] { 37 | guard let floorManager = floorManager else { return [] } 38 | return sites.isEmpty ? floorManager.facilities : floorManager.facilities.filter { $0.site == selectedSite } 39 | } 40 | 41 | /// Levels that are visible in the expanded Floor Filter levels table view 42 | /// Sort the levels by verticalOrder in a descending order 43 | var visibleLevelsInExpandedList: [AGSFloorLevel] { 44 | guard let floorManager = floorManager else { return [] } 45 | return facilities.isEmpty ? floorManager.levels : floorManager.levels.filter { 46 | $0.facility == selectedFacility 47 | }.sorted { 48 | $0.verticalOrder > $1.verticalOrder 49 | } 50 | } 51 | 52 | /// All the levels in the map 53 | /// make this property public so it can be accessible to test 54 | var allLevels: [AGSFloorLevel] { 55 | return floorManager?.levels ?? [] 56 | } 57 | 58 | /// The site, facility, and level that are selected by the user 59 | var selectedSite: AGSFloorSite? 60 | var selectedFacility: AGSFloorFacility? 61 | var selectedLevel: AGSFloorLevel? 62 | 63 | /// Gets the default level for a facility 64 | /// Uses level with vertical order 0 otherwise gets the lowest level 65 | func defaultLevel(for facility: AGSFloorFacility?) -> AGSFloorLevel? { 66 | return allLevels.first(where: { level in 67 | level.facility == facility && level.verticalOrder == .zero 68 | }) ?? lowestLevel() 69 | } 70 | 71 | /// Returns the AGSFloorLevel with the lowest verticalOrder. 72 | private func lowestLevel() -> AGSFloorLevel? { 73 | let sortedLevels = allLevels.sorted { 74 | $0.verticalOrder < $1.verticalOrder 75 | } 76 | return sortedLevels.first { 77 | $0.verticalOrder != .min && $0.verticalOrder != .max 78 | } 79 | } 80 | 81 | /// Sets the visibility of all the levels on the map based on the vertical order of the current selected level 82 | func filterMapToSelectedLevel() { 83 | guard let selectedLevel = selectedLevel else { return } 84 | allLevels.forEach { 85 | $0.isVisible = $0.verticalOrder == selectedLevel.verticalOrder 86 | } 87 | } 88 | 89 | /// Zooms to the facility if there is a selected facility, otherwise zooms to the site. 90 | func zoomToSelection() { 91 | if let selectedFacility = selectedFacility { 92 | zoom(to: selectedFacility) 93 | } else if let selectedSite = selectedSite { 94 | zoom(to: selectedSite) 95 | } 96 | } 97 | 98 | private func zoom(to floorSite: AGSFloorSite) { 99 | zoomToExtent(mapView: mapView, envelope: floorSite.geometry?.extent) 100 | } 101 | 102 | private func zoom(to floorFacility: AGSFloorFacility) { 103 | zoomToExtent(mapView: mapView, envelope: floorFacility.geometry?.extent) 104 | } 105 | 106 | private func zoomToExtent(mapView: AGSMapView?, envelope: AGSEnvelope?) { 107 | guard let mapView = mapView, 108 | let envelope = envelope 109 | else { return } 110 | 111 | let padding = 1.5 112 | let envelopeWithBuffer = AGSEnvelope( 113 | center: envelope.center, 114 | width: envelope.width * padding, 115 | height: envelope.height * padding 116 | ) 117 | 118 | if !envelopeWithBuffer.isEmpty { 119 | let viewPoint = AGSViewpoint(targetExtent: envelopeWithBuffer) 120 | mapView.setViewpoint(viewPoint, duration: 0.5, completion: nil) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/FloorFilter/SiteFacilityPromptViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGIS 17 | import Foundation 18 | 19 | /// ViewController for the site and facility prompt. 20 | final class SiteFacilityPromptViewController: UIViewController { 21 | weak var delegate: FloorFilterViewControllerDelegate? 22 | var viewModel = FloorFilterViewModel() 23 | 24 | /// UI Elements and constraints 25 | @IBOutlet var siteFacilitySearchBar: UISearchBar! 26 | @IBOutlet var backBtn: UIButton! 27 | @IBOutlet var closeBtn: UIButton! 28 | @IBOutlet var promptTitle: UILabel! 29 | @IBOutlet var promptSubtitle: UILabel! 30 | @IBOutlet var promptTitleSubtitleStackView: UIStackView! 31 | @IBOutlet var siteFacilityTableView: UITableView! 32 | @IBOutlet var designableViewHeight: NSLayoutConstraint! 33 | 34 | /// Show the facilities list directly if the map has no sites configured or if there is a previously selected facility. 35 | private var isShowingFacilities = false 36 | private var isSearchActive = false 37 | 38 | /// Filtered facilities and sites list based on search query. 39 | private var filteredSearchFacilities: [AGSFloorFacility] = [] 40 | private var filteredSearchSites: [AGSFloorSite] = [] 41 | 42 | private func filteredFacilities() -> [AGSFloorFacility] { 43 | if (isSearchActive) { 44 | return filteredSearchFacilities 45 | } else { 46 | return viewModel.facilities 47 | } 48 | } 49 | 50 | private func filteredSites() -> [AGSFloorSite] { 51 | if (isSearchActive) { 52 | return filteredSearchSites 53 | } else { 54 | return viewModel.sites 55 | } 56 | } 57 | 58 | override func viewWillAppear(_ animated: Bool) { 59 | super.viewWillAppear(animated) 60 | isShowingFacilities = (viewModel.sites.isEmpty || viewModel.selectedFacility != nil) 61 | initializeSiteFacilityTableView() 62 | } 63 | 64 | override func viewDidLoad() { 65 | super.viewDidLoad() 66 | initializeSiteFacilityTableView() 67 | initializeSiteFacilitySearchBar() 68 | initializeButtonsClickListeners() 69 | 70 | updatePromptTitle() 71 | } 72 | 73 | private func initializeButtonsClickListeners() { 74 | closeBtn.addTarget(self, action: #selector(closeSiteFacilityPrompt), for: .touchUpInside) 75 | closeBtn.isUserInteractionEnabled = true 76 | backBtn.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) 77 | backBtn.isUserInteractionEnabled = true 78 | } 79 | 80 | @objc func closeSiteFacilityPrompt() { 81 | dismiss(animated: true) 82 | } 83 | 84 | @objc func backButtonTapped() { 85 | dismissSearchBar() 86 | isShowingFacilities = false 87 | siteFacilityTableView?.reloadData() 88 | } 89 | 90 | private func updatePromptTitle() { 91 | promptTitle.font = UIFont.boldSystemFont(ofSize: 17) 92 | promptSubtitle.isHidden = true 93 | promptSubtitle.text = "" 94 | if (isShowingFacilities) { 95 | // Add the subtitle when showing facilities 96 | promptSubtitle.isHidden = false 97 | promptSubtitle.text = "Select a Facility" 98 | promptTitle.text = viewModel.selectedSite?.name ?? "" 99 | backBtn.isHidden = false 100 | } else { 101 | promptTitle?.text = viewModel.selectedSite?.name ?? "Select a Site" 102 | backBtn?.isHidden = true 103 | } 104 | } 105 | } 106 | 107 | /// Extension for Search Bar functions. 108 | extension SiteFacilityPromptViewController: UISearchBarDelegate { 109 | private func initializeSiteFacilitySearchBar() { 110 | siteFacilitySearchBar.delegate = self 111 | resetSearchFilteredResults() 112 | siteFacilitySearchBar.tintColor = .customBlue 113 | } 114 | 115 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 116 | isSearchActive = true 117 | siteFacilitySearchBar.showsCancelButton = true 118 | } 119 | 120 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 121 | isSearchActive = false 122 | siteFacilitySearchBar.resignFirstResponder() 123 | siteFacilitySearchBar.endEditing(true) 124 | } 125 | 126 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 127 | isSearchActive = false 128 | siteFacilitySearchBar.text = "" 129 | siteFacilitySearchBar.resignFirstResponder() 130 | siteFacilitySearchBar.endEditing(true) 131 | } 132 | 133 | func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { 134 | isSearchActive = false 135 | siteFacilitySearchBar.resignFirstResponder() 136 | siteFacilitySearchBar.endEditing(true) 137 | } 138 | 139 | private func resetSearchFilteredResults() { 140 | filteredSearchSites = viewModel.sites 141 | filteredSearchFacilities = viewModel.facilities 142 | } 143 | 144 | private func dismissSearchBar() { 145 | isSearchActive = false 146 | siteFacilitySearchBar.text = "" 147 | siteFacilitySearchBar.resignFirstResponder() 148 | siteFacilitySearchBar.endEditing(true) 149 | siteFacilitySearchBar.showsCancelButton = false 150 | } 151 | } 152 | 153 | /// Extension for the Sites and Facilities Table View. 154 | extension SiteFacilityPromptViewController: UITableViewDataSource, UITableViewDelegate { 155 | private func initializeSiteFacilityTableView() { 156 | siteFacilityTableView.delegate = self 157 | siteFacilityTableView.dataSource = self 158 | siteFacilityTableView.separatorStyle = .none 159 | siteFacilityTableView.register(UINib(nibName: "FloorFilterSiteFacilityCell", bundle: .module), forCellReuseIdentifier: "FloorFilterSiteFacilityCell") 160 | siteFacilityTableView.estimatedRowHeight = 45.0 161 | siteFacilityTableView.rowHeight = UITableView.automaticDimension 162 | } 163 | 164 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 165 | updatePromptTitle() 166 | return isShowingFacilities ? filteredFacilities().count : filteredSites().count 167 | } 168 | 169 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 170 | let cell = siteFacilityTableView.dequeueReusableCell(withIdentifier: "FloorFilterSiteFacilityCell", for: indexPath) as! SiteFacilityTableViewCell 171 | let sites = filteredSites() 172 | let facilities = filteredFacilities() 173 | 174 | // If there are no sites in the map, then directly show the facilities list. 175 | if (sites.isEmpty) { 176 | isShowingFacilities = true 177 | } 178 | 179 | cell.siteFacilityDotImg.isHidden = true 180 | cell.siteFacilityNameLabel?.font = UIFont(name: "Avenir", size: 16) 181 | 182 | if (isShowingFacilities) { 183 | cell.siteFacilityNameLabel.text = facilities[indexPath.row].name 184 | cell.accessoryType = .none 185 | 186 | // Highlight any previously selected Facility. 187 | if (cell.siteFacilityNameLabel.text == viewModel.selectedFacility?.name) { 188 | cell.siteFacilityDotImg.isHidden = false 189 | cell.siteFacilityNameLabel?.font = UIFont.boldSystemFont(ofSize: 16.0) 190 | } 191 | } else { 192 | cell.siteFacilityNameLabel?.text = sites[indexPath.row].name 193 | cell.accessoryType = .disclosureIndicator 194 | 195 | // If the user clicks on Back Button, then highlight any previously selected Site. 196 | if (cell.siteFacilityNameLabel.text == viewModel.selectedSite?.name) { 197 | cell.siteFacilityDotImg.isHidden = false 198 | cell.siteFacilityNameLabel?.font = UIFont.boldSystemFont(ofSize: 16.0) 199 | } 200 | } 201 | 202 | return cell 203 | } 204 | 205 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 206 | let cell = siteFacilityTableView.cellForRow(at: indexPath) as! SiteFacilityTableViewCell 207 | let sites = filteredSites() 208 | let facilities = filteredFacilities() 209 | 210 | cell.siteFacilityDotImg.isHidden = true 211 | cell.siteFacilityNameLabel?.font = UIFont.boldSystemFont(ofSize: 16.0) 212 | 213 | if (isShowingFacilities) { 214 | viewModel.selectedFacility = facilities[indexPath.row] 215 | 216 | // When a facility is selected, reset the previously selected level. 217 | viewModel.selectedLevel = nil 218 | 219 | // Close the prompt and zoom to the selected facility. 220 | closeSiteFacilityPrompt() 221 | viewModel.zoomToSelection() 222 | delegate?.siteFacilityIsUpdated(viewModel: viewModel) 223 | 224 | // Reset the search bar. 225 | resetSearchFilteredResults() 226 | dismissSearchBar() 227 | } else { 228 | viewModel.selectedSite = sites[indexPath.row] 229 | viewModel.selectedFacility = nil 230 | viewModel.selectedLevel = nil 231 | 232 | dismissSearchBar() 233 | 234 | // Zoom to the map to the selected site in case the user closes the prompt without selecting a facility. 235 | viewModel.zoomToSelection() 236 | delegate?.siteFacilityIsUpdated(viewModel: viewModel) 237 | 238 | // Reload the list to show the list of facilities for the selected site. 239 | isShowingFacilities = true 240 | updatePromptTitle() 241 | siteFacilityTableView.reloadData() 242 | } 243 | } 244 | 245 | /// Filter the sites or facilities data based on the search query. 246 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 247 | if (isShowingFacilities) { 248 | // If the search query is empty then set FilteredSearchFacilities to all the facilities in the data. 249 | filteredSearchFacilities = searchText.isEmpty ? viewModel.facilities : viewModel.facilities.filter { 250 | (facility: AGSFloorFacility) -> Bool in 251 | return facility.name.range(of: searchText, options: .caseInsensitive, range: nil, locale: nil) != nil 252 | } 253 | } else { 254 | // If the search query is empty then set FilteredSearchSites to all the sites in the data. 255 | filteredSearchSites = searchText.isEmpty ? viewModel.sites : viewModel.sites.filter { 256 | (site: AGSFloorSite) -> Bool in 257 | return site.name.range(of: searchText, options: .caseInsensitive, range: nil, locale: nil) != nil 258 | } 259 | } 260 | siteFacilityTableView.reloadData() 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/FloorFilter/SiteFacilityTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import UIKit 16 | import ArcGIS 17 | import Foundation 18 | 19 | /// Custom cell for site/facility. 20 | class SiteFacilityTableViewCell: UITableViewCell { 21 | @IBOutlet var siteFacilityNameLabel: UILabel! 22 | @IBOutlet var siteFacilityDotImg: UIImageView! 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/JobManager.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import ArcGIS 15 | import Foundation 16 | 17 | internal typealias JSONDictionary = [String: Any] 18 | public typealias JobStatusHandler = (AGSJobStatus) -> Void 19 | public typealias JobCompletionHandler = (Any?, Error?) -> Void 20 | 21 | // 22 | // MARK: JobManager 23 | 24 | private let _jobManagerSharedInstance = JobManager(jobManagerID: "shared") 25 | 26 | /** 27 | The JobManager is a class that will manage serializing kicked off Jobs to the NSUserDefaults when the app is backgrounded. 28 | Then when the JobManager is re-created on launch of an app, it will deserialize the Jobs and provide them for you via it's 29 | Job's property. 30 | 31 | The JobManager works with any AGSJob subclass. Such as AGSSyncGeodatabaseJob, AGSGenerateGeodatabaseJob, AGSExportTileCacheJob, AGSEstimateTileCacheSizeJob, AGSGenerateOfflineMapJob, AGSOfflineMapSyncJob, etc. 32 | 33 | Use the shared instance of the JobManager, or create your own with a unique ID. When you kick off a Job, register it with the JobManager. 34 | 35 | For supporting background fetch you can forward the call from your AppDelegate's 36 | `func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping` 37 | function, to the same function in this class. 38 | 39 | method. 40 | */ 41 | public class JobManager: NSObject { 42 | /// Default shared instance of the JobManager. 43 | public class var shared: JobManager { 44 | return _jobManagerSharedInstance 45 | } 46 | 47 | /// The JobManager ID, provided during initialization. 48 | public let jobManagerID: String 49 | 50 | /// Flag to signify that we shouldn't write to User Defaults. 51 | /// 52 | /// Used internally when reading stored `AGSJob`s from the User Defaults during init(). 53 | private var suppressSaveToUserDefaults = false 54 | 55 | private var kvoContext = 0 56 | 57 | /// A dictionary of Unique IDs and `AGSJob`s that the `JobManager` is managing. 58 | public private(set) var keyedJobs = [String: AGSJob]() { 59 | willSet { 60 | // Need `self` because of a Swift bug. 61 | self.keyedJobs.values.forEach { unObserveJobStatus(job: $0) } 62 | } 63 | didSet { 64 | keyedJobs.values.forEach { observeJobStatus(job: $0) } 65 | 66 | // If there was a change, then re-store the serialized AGSJobs in UserDefaults 67 | if keyedJobs != oldValue { 68 | saveJobsToUserDefaults() 69 | } 70 | } 71 | } 72 | 73 | /// A convenience accessor to the `AGSJob`s that the `JobManager` is managing. 74 | public var jobs: [AGSJob] { 75 | return Array(keyedJobs.values) 76 | } 77 | 78 | private var jobsDefaultsKey: String { 79 | return "com.esri.arcgis.runtime.toolkit.jobManager.\(jobManagerID).jobs" 80 | } 81 | 82 | private var jobStatusObservations = [String: NSKeyValueObservation]() 83 | 84 | /// Create a JobManager with an ID. 85 | /// 86 | /// - Parameter jobManagerID: An arbitrary identifier for this JobManager. 87 | public required init(jobManagerID: String) { 88 | self.jobManagerID = jobManagerID 89 | super.init() 90 | loadJobsFromUserDefaults() 91 | } 92 | 93 | deinit { 94 | keyedJobs.values.forEach { unObserveJobStatus(job: $0) } 95 | } 96 | 97 | private func toJSON() -> JSONDictionary { 98 | return keyedJobs.compactMapValues { try? $0.toJSON() } 99 | } 100 | 101 | // Observing job status code 102 | private func observeJobStatus(job: AGSJob) { 103 | let observer = job.observe(\.status, options: [.new]) { [weak self] (_, _) in 104 | self?.saveJobsToUserDefaults() 105 | } 106 | jobStatusObservations[job.serverJobID] = observer 107 | } 108 | 109 | private func unObserveJobStatus(job: AGSJob) { 110 | if let observer = jobStatusObservations[job.serverJobID] { 111 | observer.invalidate() 112 | jobStatusObservations.removeValue(forKey: job.serverJobID) 113 | } 114 | } 115 | 116 | /// Register an `AGSJob` with the `JobManager`. 117 | /// 118 | /// - Parameter job: The AGSJob to register. 119 | /// - Returns: A unique ID for the AGSJob's registration which can be used to unregister the job. 120 | @discardableResult 121 | public func register(job: AGSJob) -> String { 122 | let jobUniqueID = NSUUID().uuidString 123 | keyedJobs[jobUniqueID] = job 124 | return jobUniqueID 125 | } 126 | 127 | /// Unregister an `AGSJob` from the `JobManager`. 128 | /// 129 | /// - Parameter job: The job to unregister. 130 | /// - Returns: `true` if the job was found, `false` otherwise. 131 | @discardableResult 132 | public func unregister(job: AGSJob) -> Bool { 133 | if let jobUniqueID = keyedJobs.first(where: { $0.value === job })?.key { 134 | keyedJobs[jobUniqueID] = nil 135 | return true 136 | } 137 | return false 138 | } 139 | 140 | /// Unregister an `AGSJob` from the `JobManager`. 141 | /// 142 | /// - Parameter jobUniqueID: The job's unique ID, returned from calling `register()`. 143 | /// - Returns: `true` if the Job was found, `false` otherwise. 144 | @discardableResult 145 | public func unregister(jobUniqueID: String) -> Bool { 146 | let removed = keyedJobs.removeValue(forKey: jobUniqueID) != nil 147 | return removed 148 | } 149 | 150 | /// Clears the finished `AGSJob`s from the `JobManager`. 151 | public func clearFinishedJobs() { 152 | keyedJobs = keyedJobs.filter { 153 | let status = $0.value.status 154 | return !(status == .failed || status == .succeeded) 155 | } 156 | } 157 | 158 | /// Checks the status for all `AGSJob`s calling a completion block when completed. 159 | /// 160 | /// - Parameter completion: A completion block that is called when the status of all `AGSJob`s has been checked. Passed `true` if all statuses were retrieves successfully, or `false` otherwise. 161 | /// - Returns: An `AGSCancelable` group that can be used to cancel the status checks. 162 | @discardableResult 163 | public func checkStatusForAllJobs(completion: @escaping (Bool) -> Void) -> AGSCancelable { 164 | let cancelGroup = CancelGroup() 165 | 166 | let group = DispatchGroup() 167 | 168 | var completedWithoutErrors = true 169 | 170 | keyedJobs.forEach { 171 | group.enter() 172 | let cancellable = $0.value.checkStatus { error in 173 | if error != nil { 174 | completedWithoutErrors = false 175 | } 176 | group.leave() 177 | } 178 | cancelGroup.children.append(cancellable) 179 | } 180 | 181 | group.notify(queue: .main) { 182 | completion(completedWithoutErrors) 183 | } 184 | 185 | return cancelGroup 186 | } 187 | 188 | /// A helper function to call from a UIApplication's delegate when using iOS's Background Fetch capabilities. 189 | /// 190 | /// Checks the status for all `AGSJob`s and calls the completion handler when done. 191 | /// 192 | /// This method can be called from: 193 | /// `func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void))` 194 | /// 195 | /// See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) 196 | /// for more details. 197 | /// 198 | /// - Parameters: 199 | /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) 200 | /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) 201 | @available(iOS, deprecated: 13.0, message: "Please use 'UIApplication.shared.beginBackgroundTask(expirationHandler:)' when kicking off your job instead") 202 | public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 203 | if keyedJobs.isEmpty { 204 | return completionHandler(.noData) 205 | } else { 206 | checkStatusForAllJobs { completedWithoutErrors in 207 | if completedWithoutErrors { 208 | completionHandler(.newData) 209 | } else { 210 | completionHandler(.failed) 211 | } 212 | } 213 | } 214 | } 215 | 216 | /// Resume all paused and not-started `AGSJob`s. 217 | /// 218 | /// An `AGSJob`'s status is `.paused` when it is created from JSON. So any `AGSJob`s that have been reloaded from User Defaults will be in the `.paused` state. 219 | /// 220 | /// See the [Tasks and Jobs](https://developers.arcgis.com/ios/programming-patterns/tasks-and-jobs/#pause-resume-or-cancel-a-job) 221 | /// guide topic for more details. 222 | /// 223 | /// - Parameters: 224 | /// - statusHandler: A callback block that is called by each active `AGSJob` when the `AGSJob`'s status changes or its messages array is updated. 225 | /// - completion: A callback block that is called by each `AGSJob` when it has completed. 226 | public func resumeAllPausedJobs(statusHandler: @escaping JobStatusHandler, completion: @escaping JobCompletionHandler) { 227 | keyedJobs.lazy.filter { $0.value.status == .paused || $0.value.status == .notStarted }.forEach { 228 | $0.value.start(statusHandler: statusHandler, completion: completion) 229 | } 230 | } 231 | 232 | /// Pauses any currently running job. 233 | public func pauseAllJobs() { 234 | keyedJobs.values.forEach { 235 | guard $0.status == .started else { return } 236 | $0.progress.pause() 237 | } 238 | } 239 | 240 | /// Saves all managed `AGSJob`s to User Defaults. 241 | /// 242 | /// This happens automatically when the `AGSJob`s are registered/unregistered. 243 | /// It also happens when an `AGSJob`'s status changes. 244 | private func saveJobsToUserDefaults() { 245 | guard !suppressSaveToUserDefaults else { return } 246 | 247 | UserDefaults.standard.set(self.toJSON(), forKey: jobsDefaultsKey) 248 | } 249 | 250 | /// Load any `AGSJob`s that have been saved to User Defaults. 251 | /// 252 | /// This happens when the `JobManager` is initialized. All `AGSJob`s will be in the `.paused` state when first restored from JSON. 253 | /// 254 | /// See the [Tasks and Jobs](https://developers.arcgis.com/ios/programming-patterns/tasks-and-jobs/#pause-resume-or-cancel-a-job) 255 | /// guide topic for more details. 256 | private func loadJobsFromUserDefaults() { 257 | if let storedJobsJSON = UserDefaults.standard.dictionary(forKey: jobsDefaultsKey) { 258 | suppressSaveToUserDefaults = true 259 | keyedJobs = storedJobsJSON.compactMapValues { $0 is JSONDictionary ? (try? AGSJob.fromJSON($0)) as? AGSJob : nil } 260 | suppressSaveToUserDefaults = false 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/MapViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import ArcGIS 16 | 17 | open class MapViewController: UIViewController { 18 | public let mapView = AGSMapView(frame: CGRect.zero) 19 | 20 | override open func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | mapView.frame = view.bounds 24 | mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 25 | view.addSubview(mapView) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward132.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Backward88.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Backward44.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Backward88.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Backward132.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iOS8_NorthCompass36_D.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iOS8_NorthCompass72.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iOS8_NorthCompass108.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass108.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass36_D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass36_D.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/CompassIcon.imageset/iOS8_NorthCompass72.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Dot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "compression-type" : "automatic", 5 | "filename" : "fiber_manual_record_black_18dp.svg", 6 | "idiom" : "universal", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Dot.imageset/fiber_manual_record_black_18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Forward44.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Forward88.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Forward132.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward132.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Forward.imageset/Forward88.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iOS8_Toolbar_MeasureArea22.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iOS8_Toolbar_MeasureArea44.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iOS8_Toolbar_MeasureArea66.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea22.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureArea.imageset/iOS8_Toolbar_MeasureArea66.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "MeasureFeature22.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "MeasureFeature44.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "MeasureFeature66.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature22.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureFeature.imageset/MeasureFeature66.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iOS8_Toolbar_MeasureLength22.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iOS8_Toolbar_MeasureLength44.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iOS8_Toolbar_MeasureLength66.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength22.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/MeasureLength.imageset/iOS8_Toolbar_MeasureLength66.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Pause44.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Pause88.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Pause132.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause132.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Pause.imageset/Pause88.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Play44.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Play88.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Play132.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play132.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play44.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Play.imageset/Play88.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iOS8_TabBar_Redo22s.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iOS8_TabBar_Redo44s.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iOS8_TabBar_Redo66s.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo22s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo22s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo44s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo44s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo66s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Redo.imageset/iOS8_TabBar_Redo66s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Site.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "business_black_18dp (1).svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "business_black_24dp (1).svg", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "business_black_24dp (1)-1.svg", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Site.imageset/business_black_18dp (1).svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Site.imageset/business_black_24dp (1)-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Site.imageset/business_black_24dp (1).svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iOS8_TabBar_Undo22s.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iOS8_TabBar_Undo44s.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iOS8_TabBar_Undo66s.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo22s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo22s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo44s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo44s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo66s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/arcgis-runtime-toolkit-ios/3919a18208679aa47deec2f2e9218836c4bb9933/Sources/ArcGISToolkit/Resources/Assets.xcassets/Undo.imageset/iOS8_TabBar_Undo66s.png -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/FloorFilterSiteFacilityCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyTrackingDomains 17 | 18 | NSPrivacyTracking 19 | 20 | NSPrivacyCollectedDataTypes 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | 16 | open class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 17 | public var cellReuseIdentifier = "cell" 18 | public var tableView = UITableView(frame: .zero) 19 | 20 | override open func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | tableView.translatesAutoresizingMaskIntoConstraints = false 24 | view.addSubview(tableView) 25 | 26 | NSLayoutConstraint.activate([ 27 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 28 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 29 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 30 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 31 | ]) 32 | 33 | tableView.delegate = self 34 | tableView.dataSource = self 35 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier) 36 | } 37 | 38 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | return 0 40 | } 41 | 42 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 43 | return UITableViewCell() 44 | } 45 | 46 | public func goBack(_ completion: (() -> Void)? ) { 47 | if let nc = navigationController { 48 | nc.popViewController(animated: true) 49 | completion?() 50 | } else { 51 | self.dismiss(animated: true) { 52 | completion?() 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ArcGISToolkit/UnitsViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Esri. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | import UIKit 15 | import class ArcGIS.AGSUnit 16 | 17 | /// The protocol you implement to respond as the user interacts with the units 18 | /// view controller. 19 | public protocol UnitsViewControllerDelegate: AnyObject { 20 | /// Tells the delegate that the user has cancelled selecting a unit. 21 | /// 22 | /// - Parameter unitsViewController: The current units view controller. 23 | func unitsViewControllerDidCancel(_ unitsViewController: UnitsViewController) 24 | /// Tells the delegate that the user has selected a unit. 25 | /// 26 | /// - Parameters: 27 | /// - unitsViewController: The current units view controller. 28 | func unitsViewControllerDidSelectUnit(_ unitsViewController: UnitsViewController) 29 | } 30 | 31 | /// A view controller for selecting a unit from a list of units. 32 | public class UnitsViewController: TableViewController { 33 | /// The delegate of the units view controller. 34 | public weak var delegate: UnitsViewControllerDelegate? 35 | 36 | /// The units presented to the user. 37 | public var units = [AGSUnit]() { 38 | didSet { 39 | guard isViewLoaded else { return } 40 | tableView.reloadData() 41 | } 42 | } 43 | 44 | /// The currently selected unit. 45 | public var selectedUnit: AGSUnit? { 46 | didSet { 47 | guard selectedUnit != oldValue else { return } 48 | selectedUnitDidChange(oldValue) 49 | } 50 | } 51 | 52 | /// The units that match the search predicate or `nil` if the search field 53 | /// is empty. 54 | private var filteredUnits: [AGSUnit]? { 55 | didSet { 56 | guard filteredUnits != oldValue else { return } 57 | tableView.reloadData() 58 | } 59 | } 60 | 61 | /// Called in response to the Cancel button being tapped. 62 | @objc 63 | private func cancel() { 64 | // If the search controller is still active, the delegate will not be 65 | // able to dismiss this if they showed this modally. 66 | // (or wrapped it in a navigation controller and showed that modally) 67 | // Only do this if not being presented from a nav controller 68 | // as in that case, it causes problems when the delegate that pushed this VC 69 | // tries to pop it off the stack. 70 | if presentingViewController != nil { 71 | navigationItem.searchController?.isActive = false 72 | } 73 | delegate?.unitsViewControllerDidCancel(self) 74 | } 75 | 76 | /// Called in response to `selectedUnit` changing. 77 | /// 78 | /// - Parameter previousSelectedUnit: The previous value of `selectedUnit`. 79 | private func selectedUnitDidChange(_ previousSelectedUnit: AGSUnit?) { 80 | guard isViewLoaded else { return } 81 | var indexPaths = [IndexPath]() 82 | if let unit = previousSelectedUnit, let indexPath = indexPath(for: unit) { 83 | indexPaths.append(indexPath) 84 | } 85 | if let unit = selectedUnit, let indexPath = indexPath(for: unit) { 86 | indexPaths.append(indexPath) 87 | } 88 | guard !indexPaths.isEmpty else { return } 89 | tableView.reloadRows(at: indexPaths, with: .automatic) 90 | } 91 | 92 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 93 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 94 | sharedInitialization() 95 | } 96 | 97 | public required init?(coder aDecoder: NSCoder) { 98 | super.init(coder: aDecoder) 99 | sharedInitialization() 100 | } 101 | 102 | private func sharedInitialization() { 103 | title = "Units" 104 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(UnitsViewController.cancel)) 105 | definesPresentationContext = true 106 | navigationItem.searchController = makeSearchController() 107 | } 108 | 109 | /// Creates a search controller for searching the list of units. 110 | /// 111 | /// - Returns: A configured search controller. 112 | private func makeSearchController() -> UISearchController { 113 | let searchController = UISearchController(searchResultsController: nil) 114 | searchController.obscuresBackgroundDuringPresentation = false 115 | searchController.hidesNavigationBarDuringPresentation = false 116 | searchController.searchResultsUpdater = self 117 | 118 | let searchBar = searchController.searchBar 119 | searchBar.spellCheckingType = .no 120 | searchBar.autocapitalizationType = .none 121 | searchBar.autocorrectionType = .no 122 | 123 | return searchController 124 | } 125 | 126 | override public func viewDidLoad() { 127 | super.viewDidLoad() 128 | } 129 | 130 | // MARK: TableView delegate/datasource methods 131 | 132 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 133 | // when the user taps on a unit 134 | // 135 | tableView.deselectRow(at: indexPath, animated: true) 136 | let unit = unitForCell(at: indexPath) 137 | guard unit != selectedUnit else { return } 138 | selectedUnit = unit 139 | // If the search controller is still active, the delegate will not be 140 | // able to dismiss this if they showed this modally. 141 | // (or wrapped it in a navigation controller and showed that modally) 142 | // Only do this if not being presented from a nav controller 143 | // as in that case, it causes problems when the delegate that pushed this VC 144 | // tries to pop it off the stack. 145 | if presentingViewController != nil { 146 | navigationItem.searchController?.isActive = false 147 | } 148 | delegate?.unitsViewControllerDidSelectUnit(self) 149 | } 150 | 151 | override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 152 | return filteredUnits?.count ?? units.count 153 | } 154 | 155 | override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 156 | let unit = unitForCell(at: indexPath) 157 | let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) 158 | cell.textLabel?.text = unit.pluralDisplayName 159 | cell.accessoryType = unit == selectedUnit ? .checkmark : .none 160 | return cell 161 | } 162 | 163 | // MARK: IndexPath -> Info 164 | 165 | /// Returns the index path of the cell corresponding to the given unit. 166 | /// 167 | /// - Parameter unit: A unit. 168 | /// - Returns: An index path or `nil` if there is no cell corresponding to 169 | /// the given unit. 170 | private func indexPath(for unit: AGSUnit) -> IndexPath? { 171 | if let filteredUnits = filteredUnits { 172 | if let row = filteredUnits.firstIndex(of: unit) { 173 | return IndexPath(row: row, section: 0) 174 | } else { 175 | return nil 176 | } 177 | } else if let row = units.firstIndex(of: unit) { 178 | return IndexPath(row: row, section: 0) 179 | } else { 180 | return nil 181 | } 182 | } 183 | 184 | /// The unit for the cell at the given index path. 185 | /// 186 | /// - Parameter indexPath: An index path. 187 | /// - Returns: The unit corresponding to the cell at the given index path. 188 | private func unitForCell(at indexPath: IndexPath) -> AGSUnit { 189 | return filteredUnits?[indexPath.row] ?? units[indexPath.row] 190 | } 191 | } 192 | 193 | extension UnitsViewController: UISearchResultsUpdating { 194 | public func updateSearchResults(for searchController: UISearchController) { 195 | if let text = searchController.searchBar.text?.trimmingCharacters(in: .whitespaces), 196 | !text.isEmpty { 197 | filteredUnits = units.filter { 198 | $0.pluralDisplayName.range(of: text, options: .caseInsensitive) != nil 199 | } 200 | } else { 201 | filteredUnits = nil 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/ArcGISToolkitTests/ArcGISToolkitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | import ArcGISToolkit 17 | import ArcGIS 18 | 19 | class ArcGISToolkitTests: XCTestCase { 20 | func testComponentCreation() { 21 | // Compass 22 | let compass = Compass(mapView: AGSMapView(frame: .zero)) 23 | XCTAssertNotNil(compass) 24 | 25 | // LegendViewController 26 | var legendVC = LegendViewController.makeLegendViewController(geoView: AGSMapView(frame: .zero)) 27 | XCTAssertNotNil(legendVC) 28 | 29 | legendVC = LegendViewController.makeLegendViewController() 30 | XCTAssertNotNil(legendVC) 31 | 32 | // MeasureToolbar 33 | let measureToolbar = MeasureToolbar(mapView: AGSMapView(frame: .zero)) 34 | XCTAssertNotNil(measureToolbar) 35 | 36 | // Scalebar 37 | let scaleBar = Scalebar(mapView: AGSMapView(frame: .zero)) 38 | XCTAssertNotNil(scaleBar) 39 | 40 | // UnitsViewController 41 | let unitsVC = UnitsViewController() 42 | XCTAssertNotNil(unitsVC) 43 | 44 | // Job Manager 45 | let jobManager = JobManager.shared 46 | XCTAssertNotNil(jobManager) 47 | 48 | // PopupController 49 | let vc = UIViewController() 50 | let popupController = PopupController(geoViewController: vc, geoView: AGSMapView(frame: .zero)) 51 | XCTAssertNotNil(popupController) 52 | 53 | // TimeSlider 54 | let timeSlider = TimeSlider() 55 | XCTAssertNotNil(timeSlider) 56 | 57 | // BookarksViewController 58 | var bookmarksVC = BookmarksViewController(geoView: AGSMapView(frame: .zero)) 59 | XCTAssertNotNil(bookmarksVC) 60 | 61 | let bookmark = AGSBookmark(name: "Barcelona", viewpoint: AGSViewpoint(latitude: 41.385063, longitude: 2.173404, scale: 6e5)) 62 | bookmarksVC = BookmarksViewController(bookmarks: [bookmark]) 63 | XCTAssertNotNil(bookmarksVC) 64 | 65 | // FloorFilterView 66 | let floorFilterVC = FloorFilterViewController.makeFloorFilterView(geoView: AGSMapView(frame: .zero)) 67 | XCTAssertNotNil(floorFilterVC) 68 | } 69 | } 70 | 71 | /// Helper function to load an `AGSLoadable` object, waiting until it's loaded to return. 72 | /// - Parameter object: The loadable object. 73 | public func XCTLoad(_ object: AGSLoadable, file: StaticString = #file, line: UInt = #line) { 74 | // Wait for the object to load. 75 | let loadExp = XCTestExpectation(description: "expectation for `object.load`") 76 | object.load { (error) in 77 | XCTAssertNil(error, file: file, line: line) 78 | loadExp.fulfill() 79 | } 80 | let waitResult = XCTWaiter().wait(for: [loadExp], timeout: 5.0) 81 | XCTAssertEqual(waitResult, .completed, file: file, line: line) 82 | } 83 | -------------------------------------------------------------------------------- /Tests/ArcGISToolkitTests/BookmarksViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2019 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | import ArcGISToolkit 17 | import ArcGIS 18 | 19 | class BookmarksViewControllerTests: XCTestCase { 20 | override func setUp() async throws { 21 | AGSArcGISRuntimeEnvironment.apiKey = "<#API Key#>" 22 | } 23 | 24 | /// Tests the creation of the `BookmarksViewController` using a list of `AGSBookmark`. 25 | func testBookmarksList() { 26 | let bookmarks = generateBookmarks() 27 | let bookmarksVC = BookmarksViewController(bookmarks: bookmarks) 28 | XCTAssertEqual(bookmarks.count, bookmarksVC.bookmarks.count) 29 | } 30 | 31 | /// Tests the creation of the `BookmarksViewController` using an `AGSMapView` by verifying the `bookmarks` property returns the correct number of bookmarks. 32 | func testBookmarksMapView() { 33 | let mapView = AGSMapView() 34 | let map = AGSMap(basemapStyle: .arcGISStreets) 35 | mapView.map = map 36 | 37 | let bookmarks = generateBookmarks() 38 | map.bookmarks.addObjects(from: bookmarks) 39 | 40 | // Wait for the map to load. This allows the observers to be set up. 41 | XCTLoad(map) 42 | 43 | // Create the BookmarksViewController. 44 | let bookmarksVC = BookmarksViewController(geoView: mapView) 45 | XCTAssertEqual(map.bookmarks.count, bookmarksVC.bookmarks.count) 46 | } 47 | 48 | /// Tests the ability to detect changes to the map and bookmarks array in order to update the list of bookmarks. 49 | func testChangeMapAndBookmarks() { 50 | let mapView = AGSMapView() 51 | let map = AGSMap(basemapStyle: .arcGISStreets) 52 | mapView.map = map 53 | 54 | let bookmarks = generateBookmarks() 55 | map.bookmarks.addObjects(from: bookmarks) 56 | 57 | // Wait for the map to load. This allows the observers to be set up. 58 | XCTLoad(map) 59 | 60 | // Create the BookmarksViewController. 61 | let bookmarksVC = BookmarksViewController(geoView: mapView) 62 | 63 | // Change the map. 64 | let newMap = AGSMap(basemapStyle: .arcGISImagery) 65 | newMap.bookmarks.add(AGSBookmark(name: "Mysterious Desert Pattern", viewpoint: AGSViewpoint(latitude: 27.3805833, longitude: 33.6321389, scale: 6e3))) 66 | mapView.map = newMap 67 | 68 | // Wait for the map to load. This allows the observers to be set up. 69 | XCTLoad(newMap) 70 | 71 | // Check if bookmarks property is updated. 72 | XCTAssertEqual(newMap.bookmarks.count, bookmarksVC.bookmarks.count) 73 | 74 | // Add bookmark to list. 75 | mapView.map?.bookmarks.add(AGSBookmark(name: "Strange Symbol", viewpoint: AGSViewpoint(latitude: 37.401573, longitude: -116.867808, scale: 6e3))) 76 | 77 | // Wait for a bit so the bookmark-changed notification can be propagated. 78 | let exp = XCTestExpectation(description: "generic wait...") 79 | XCTWaiter().wait(for: [exp], timeout: 2.0) 80 | 81 | // Check if bookmarks property is updated. 82 | XCTAssertEqual(newMap.bookmarks.count, bookmarksVC.bookmarks.count) 83 | } 84 | 85 | /// Tests the creation of the `BookmarksViewController` using an `AGSSceneView` by verifying the `bookmarks` property returns the correct number of bookmarks 86 | func testBookmarksSceneView() { 87 | let sceneView = AGSSceneView() 88 | let scene = AGSScene(basemapStyle: .arcGISStreets) 89 | sceneView.scene = scene 90 | 91 | let bookmarks = generateBookmarks() 92 | scene.bookmarks.addObjects(from: bookmarks) 93 | 94 | // Wait for the scene to load. This allows the observers to be set up. 95 | XCTLoad(scene) 96 | 97 | // Create the BookmarksViewController. 98 | let bookmarksVC = BookmarksViewController(geoView: sceneView) 99 | XCTAssertEqual(scene.bookmarks.count, bookmarksVC.bookmarks.count) 100 | } 101 | 102 | /// Tests the ability to detect changes to the scene and bookmarks array in order to update the list of bookmarks. 103 | func testChangeSceneAndBookmarks() { 104 | let sceneView = AGSSceneView() 105 | let scene = AGSScene(basemapStyle: .arcGISStreets) 106 | sceneView.scene = scene 107 | 108 | let bookmarks = generateBookmarks() 109 | scene.bookmarks.addObjects(from: bookmarks) 110 | 111 | // Wait for the scene to load. This allows the observers to be set up. 112 | XCTLoad(scene) 113 | 114 | // Create the BookmarksViewController. 115 | let bookmarksVC = BookmarksViewController(geoView: sceneView) 116 | 117 | // Change the scene. 118 | let newScene = AGSScene(basemapStyle: .arcGISImagery) 119 | newScene.bookmarks.add(AGSBookmark(name: "Mysterious Desert Pattern", viewpoint: AGSViewpoint(latitude: 27.3805833, longitude: 33.6321389, scale: 6e3))) 120 | sceneView.scene = newScene 121 | 122 | // Wait for the scene to load. This allows the observers to be set up. 123 | XCTLoad(newScene) 124 | 125 | // Check if bookmarks property is updated. 126 | XCTAssertEqual(newScene.bookmarks.count, bookmarksVC.bookmarks.count) 127 | 128 | // Add bookmark to list. 129 | sceneView.scene?.bookmarks.add(AGSBookmark(name: "Strange Symbol", viewpoint: AGSViewpoint(latitude: 37.401573, longitude: -116.867808, scale: 6e3))) 130 | 131 | // Wait for a bit so the bookmark-changed notification can be propagated. 132 | let exp = XCTestExpectation(description: "generic wait...") 133 | XCTWaiter().wait(for: [exp], timeout: 2.0) 134 | 135 | // Check if bookmarks property is updated. 136 | XCTAssertEqual(newScene.bookmarks.count, bookmarksVC.bookmarks.count) 137 | } 138 | 139 | /// Tests changing the mapView and ensuring the `bookmarks` property is updated. 140 | func testChangeMapView() { 141 | let mapView = AGSMapView() 142 | let map = AGSMap(basemapStyle: .arcGISStreets) 143 | mapView.map = map 144 | 145 | let bookmarks = generateBookmarks() 146 | map.bookmarks.addObjects(from: bookmarks) 147 | 148 | // Wait for the map to load. This allows the observers to be set up. 149 | XCTLoad(map) 150 | 151 | // Create the BookmarksViewController. 152 | let bookmarksVC = BookmarksViewController(geoView: mapView) 153 | 154 | // Verify the bookmarks are set, so we know when we change them. 155 | XCTAssertEqual(map.bookmarks.count, bookmarksVC.bookmarks.count) 156 | 157 | // Change the mapView. 158 | let newMapView = AGSMapView() 159 | let newMap = AGSMap(basemapStyle: .arcGISImagery) 160 | newMapView.map = newMap 161 | 162 | // Create a new array of `AGSBookmark` and add to map. 163 | var newBookmarks = [AGSBookmark(name: "Mysterious Desert Pattern", viewpoint: AGSViewpoint(latitude: 27.3805833, longitude: 33.6321389, scale: 6e3))] 164 | newBookmarks.append(contentsOf: generateBookmarks()) 165 | newMap.bookmarks.addObjects(from: newBookmarks) 166 | 167 | // Set the new map view on the bookmarks view controller. 168 | bookmarksVC.geoView = newMapView 169 | 170 | // Wait for the map to load. This allows the observers to be set up. 171 | XCTLoad(newMap) 172 | 173 | // Check if bookmarks property is updated. 174 | XCTAssertEqual(newMap.bookmarks.count, bookmarksVC.bookmarks.count) 175 | } 176 | 177 | /// Tests changing the sceneView and ensuring the `bookmarks` property is updated. 178 | func testChangeSceneView() { 179 | let sceneView = AGSSceneView() 180 | let scene = AGSScene(basemapStyle: .arcGISStreets) 181 | sceneView.scene = scene 182 | 183 | let bookmarks = generateBookmarks() 184 | scene.bookmarks.addObjects(from: bookmarks) 185 | 186 | // Wait for the scene to load. This allows the observers to be set up. 187 | XCTLoad(scene) 188 | 189 | // Create the BookmarksViewController. 190 | let bookmarksVC = BookmarksViewController(geoView: sceneView) 191 | 192 | // Verify the bookmarks are set, so we know when we change them. 193 | XCTAssertEqual(scene.bookmarks.count, bookmarksVC.bookmarks.count) 194 | 195 | // Change the sceneView. 196 | let newSceneView = AGSSceneView() 197 | let newScene = AGSScene(basemapStyle: .arcGISImagery) 198 | newSceneView.scene = newScene 199 | 200 | // Create a new array of `AGSBookmark` and add to map. 201 | var newBookmarks = [AGSBookmark(name: "Mysterious Desert Pattern", viewpoint: AGSViewpoint(latitude: 27.3805833, longitude: 33.6321389, scale: 6e3))] 202 | newBookmarks.append(contentsOf: generateBookmarks()) 203 | newScene.bookmarks.addObjects(from: newBookmarks) 204 | 205 | // Set the new scene view on the bookmarks view controller. 206 | bookmarksVC.geoView = newSceneView 207 | 208 | // Wait for the scene to load. This allows the observers to be set up. 209 | XCTLoad(newScene) 210 | 211 | // Check if bookmarks property is updated. 212 | XCTAssertEqual(newScene.bookmarks.count, bookmarksVC.bookmarks.count) 213 | } 214 | 215 | /// Generates a list of predefined bookmarks for testing. 216 | func generateBookmarks() -> [AGSBookmark] { 217 | return [AGSBookmark(name: "Barcelona", viewpoint: AGSViewpoint(latitude: 41.385063, longitude: 2.173404, scale: 6e5)), 218 | AGSBookmark(name: "Portland", viewpoint: AGSViewpoint(latitude: 44.977753, longitude: -93.265015, scale: 6e5)), 219 | AGSBookmark(name: "Minneapolis", viewpoint: AGSViewpoint(latitude: 44.977753, longitude: -93.265015, scale: 6e5)), 220 | AGSBookmark(name: "Edinburgh", viewpoint: AGSViewpoint(latitude: 55.953251, longitude: -3.188267, scale: 6e5))] 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Tests/ArcGISToolkitTests/FloorFilterViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Esri. 3 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | @testable import ArcGISToolkit 17 | import ArcGIS 18 | 19 | final class FloorFilterViewModelTests: XCTestCase { 20 | func testSitesData() throws { 21 | let portal = AGSPortal(url: URL(string: "https://indoors.maps.arcgis.com/")!, loginRequired: false) 22 | let portalItem = AGSPortalItem(portal: portal, itemID: "f133a698536f44c8884ad81f80b6cfc7") 23 | let map = AGSMap(item: portalItem) 24 | let mapView = AGSMapView() 25 | mapView.map = map 26 | 27 | XCTLoad(map) 28 | let floorManager = try XCTUnwrap(map.floorManager) 29 | XCTLoad(floorManager) 30 | let viewModel = FloorFilterViewModel() 31 | viewModel.mapView = mapView 32 | XCTAssertEqual(viewModel.sites.count, 1) 33 | } 34 | 35 | func testFacilitiesData() throws { 36 | let portal = AGSPortal(url: URL(string: "https://indoors.maps.arcgis.com/")!, loginRequired: false) 37 | let portalItem = AGSPortalItem(portal: portal, itemID: "f133a698536f44c8884ad81f80b6cfc7") 38 | let map = AGSMap(item: portalItem) 39 | let mapView = AGSMapView() 40 | mapView.map = map 41 | 42 | XCTLoad(map) 43 | let floorManager = try XCTUnwrap(map.floorManager) 44 | XCTLoad(floorManager) 45 | let viewModel = FloorFilterViewModel() 46 | viewModel.mapView = mapView 47 | viewModel.selectedSite = viewModel.sites.first 48 | XCTAssertEqual(viewModel.facilities.count, 1) 49 | } 50 | 51 | func testLevelsData() throws { 52 | let portal = AGSPortal(url: URL(string: "https://indoors.maps.arcgis.com/")!, loginRequired: false) 53 | let portalItem = AGSPortalItem(portal: portal, itemID: "f133a698536f44c8884ad81f80b6cfc7") 54 | let map = AGSMap(item: portalItem) 55 | let mapView = AGSMapView() 56 | mapView.map = map 57 | 58 | XCTLoad(map) 59 | let floorManager = try XCTUnwrap(map.floorManager) 60 | XCTLoad(floorManager) 61 | let viewModel = FloorFilterViewModel() 62 | viewModel.mapView = mapView 63 | XCTAssertEqual(viewModel.allLevels.count, 3) 64 | } 65 | } 66 | --------------------------------------------------------------------------------