From f31edbb7f2742f30a0f4ed34493bab13f8f6913c Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 21 Nov 2019 12:20:13 -0600 Subject: [PATCH 01/47] DataSource class and tests. --- .../ArcGISToolkit.xcodeproj/project.pbxproj | 8 ++ Toolkit/ArcGISToolkit/DataSource.swift | 96 ++++++++++++++++ .../ArcGISToolkitTests/DataSourceTests.swift | 104 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 Toolkit/ArcGISToolkit/DataSource.swift create mode 100644 Toolkit/ArcGISToolkitTests/DataSourceTests.swift diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index da57357b..66d76dfd 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ E46893291FEDAE36008ADA79 /* Compass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893281FEDAE36008ADA79 /* Compass.swift */; }; E46EF2C1236C8A8600A8C50B /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46EF2BF236C8A8600A8C50B /* BookmarksViewController.swift */; }; E46EF2C7236CA16800A8C50B /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46EF2C5236CA16800A8C50B /* BookmarksTableViewController.swift */; }; + E47DDD2C23870C8E000414D1 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47DDD2B23870C8D000414D1 /* DataSource.swift */; }; + E47DDD2E23870C97000414D1 /* DataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47DDD2D23870C97000414D1 /* DataSourceTests.swift */; }; E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405721E9BE7B700927208 /* LegendViewController.swift */; }; E484057A1E9C262D00927208 /* Legend.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E48405791E9C262D00927208 /* Legend.storyboard */; }; E4ED3D8E237F19B800D835F6 /* BookmarksViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */; }; @@ -83,6 +85,8 @@ E46893281FEDAE36008ADA79 /* Compass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compass.swift; sourceTree = ""; }; E46EF2BF236C8A8600A8C50B /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = ""; }; E46EF2C5236CA16800A8C50B /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = ""; }; + E47DDD2B23870C8D000414D1 /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; + E47DDD2D23870C97000414D1 /* DataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceTests.swift; sourceTree = ""; }; E48405721E9BE7B700927208 /* LegendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendViewController.swift; sourceTree = ""; }; E48405791E9C262D00927208 /* Legend.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Legend.storyboard; sourceTree = ""; }; E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewControllerTests.swift; sourceTree = ""; }; @@ -150,6 +154,7 @@ 88B689ED1E96EF0800B67FAB /* Misc */ = { isa = PBXGroup; children = ( + E47DDD2B23870C8D000414D1 /* DataSource.swift */, 88B689F01E96EFD700B67FAB /* Extensions.swift */, 88B689F31E96EFD700B67FAB /* MapViewController.swift */, 88B689FA1E96EFD700B67FAB /* TableViewController.swift */, @@ -190,6 +195,7 @@ E4685B04237CA3C400F168CF /* ArcGISToolkitTests */ = { isa = PBXGroup; children = ( + E47DDD2D23870C97000414D1 /* DataSourceTests.swift */, E4685B05237CA3C400F168CF /* ArcGISToolkitTests.swift */, E4685B07237CA3C400F168CF /* Info.plist */, E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */, @@ -347,6 +353,7 @@ 2140781C209B628200FBFDCC /* TimeSlider.swift in Sources */, 88DBC2A31FE83DB800255921 /* CancelGroup.swift in Sources */, 88B68A071E96EFD700B67FAB /* TableViewController.swift in Sources */, + E47DDD2C23870C8E000414D1 /* DataSource.swift in Sources */, 88B689FD1E96EFD700B67FAB /* Extensions.swift in Sources */, 88B68A741E9D7F6300B67FAB /* Coalescer.swift in Sources */, 88B68A001E96EFD700B67FAB /* MapViewController.swift in Sources */, @@ -359,6 +366,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E47DDD2E23870C97000414D1 /* DataSourceTests.swift in Sources */, E4ED3D8E237F19B800D835F6 /* BookmarksViewControllerTests.swift in Sources */, E4685B06237CA3C400F168CF /* ArcGISToolkitTests.swift in Sources */, ); diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift new file mode 100644 index 00000000..be93d86a --- /dev/null +++ b/Toolkit/ArcGISToolkit/DataSource.swift @@ -0,0 +1,96 @@ +// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import ArcGIS + +/// The data source is used to represent an array of `AGSLayerContent` for use in a variety of implementations. It is iinitialized with either an array of `AGSLayerContent` +/// or an `AGSGeoView` of which the operational and base map layers (`AGSLayerContent`) are identified. +public class DataSource: NSObject { + /// Returns a `DataSource` initialized with the given `AGSLayerContent` array.. + /// - Parameter layers: The array of `AGSLayerContent`. + /// - Since: 100.7.0 + public init(layers: [AGSLayerContent]) { + super.init() + layerContents.append(contentsOf: layers) + } + + /// Returns a `DataSource` initialized with the operational and base map layers of a map or scene in an `AGSGeoView`. + /// - Parameter geoView: The `AGSGeoView` containing the map/scene's operational and base map layers. + /// - Since: 100.7.0 + public init(geoView: AGSGeoView) { + super.init() + self.geoView = geoView + geoViewDidChange(nil) + } + + /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and base map layers to use as data. + /// If the `DataSource` was initialized with an array of `AGSLayerContent`, `goeView` will be nil. + /// - Since: 100.7.0 + public private(set) var geoView: AGSGeoView? { + didSet { + geoViewDidChange(oldValue) + } + } + + /// The list of all layers used to generate the TOC/Legend, read-only. It contains both the operational layers of the map/scene and + /// the reference and base layers of the basemap. The order of the layer contents is the order in which they are drawn + /// in a map or scene: bottom up (the first layer in the array is at the bottom and drawn first; the last layer is at the top and drawn last). + /// - Since: 100.7.0 + public private(set) var layerContents = [AGSLayerContent]() + + private func geoViewDidChange(_ previousGeoView: AGSGeoView?) { + if let mapView = geoView as? AGSMapView { + mapView.map?.load { [weak self] (error) in + guard let self = self, let mapView = self.geoView as? AGSMapView else { return } + if let error = error { + print("Error loading map: \(error)") + } else { + self.layerContents = mapView.map?.operationalLayers as? [AGSLayerContent] ?? [] + self.appendBasemap(mapView.map?.basemap) + } + } + } else if let sceneView = geoView as? AGSSceneView { + sceneView.scene?.load { [weak self] (error) in + guard let self = self, let sceneView = self.geoView as? AGSSceneView else { return } + if let error = error { + print("Error loading map: \(error)") + } else { + self.layerContents = sceneView.scene?.operationalLayers as? [AGSLayerContent] ?? [] + self.appendBasemap(sceneView.scene?.basemap) + } + } + } + } + + func appendBasemap(_ basemap: AGSBasemap?) { + guard let basemap = basemap else { return } + + basemap.load { [weak self] (error) in + if let error = error { + print("Error loading base map: \(error)") + } else { + // Append any reference layers to the `layerContents` array. + if let referenceLayers = basemap.referenceLayers as? [AGSLayerContent] { + self?.layerContents.append(contentsOf: referenceLayers) + } + + // Insert any base layers at the beginning of the `layerContents` array. + if let baseLayers = basemap.baseLayers as? [AGSLayerContent] { + self?.layerContents.insert(contentsOf: baseLayers, at: 0) + } + } + } + } +} diff --git a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift new file mode 100644 index 00000000..75b9218d --- /dev/null +++ b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift @@ -0,0 +1,104 @@ +// +// Copyright 2019 Esri. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest +import ArcGISToolkit +import ArcGIS + +class DataSourceTests: XCTestCase { + /// Tests the creation of a `DataSource` using a list of `AGSLayerContent`. + func testDataSourceLayers() { + let layers = generateLayerContents() + let dataSource = DataSource(layers: layers) + XCTAssertEqual(layers.count, dataSource.layerContents.count) + } + + /// Tests the creation of a `DataSource` using an `AGSMapView` by verifying the `layerContents` property returns the correct number of `AGSLayerContent`. + func testDataSourceMapView() { + let mapView = AGSMapView() + let map = AGSMap(basemap: .streets()) + mapView.map = map + + let layerContents = generateLayerContents() + map.operationalLayers.addObjects(from: layerContents) + + // Wait for the map to load. This allows the observers to be set up. + XCTLoad(map) + + // Create the dataSource. + let dataSource = DataSource(geoView: mapView) + XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) + } + + /// Tests the creation of the `dataSource` using an `AGSSceneView` by verifying the `layerContents` property returns the correct number of layerContents + func testlayerContentsSceneView() { + let sceneView = AGSSceneView() + let scene = AGSScene(basemap: .streets()) + sceneView.scene = scene + + let layerContents = generateLayerContents() + scene.operationalLayers.addObjects(from: layerContents) + + // Wait for the scene to load. This allows the observers to be set up. + XCTLoad(scene) + + // Create the dataSource. + let dataSource = DataSource(geoView: sceneView) + XCTAssertEqual(layerCount(scene: scene), dataSource.layerContents.count) + } + + /// Generates a list of predefined layerContents for testing. + func generateLayerContents() -> [AGSLayerContent] { + let featureTables: [AGSFeatureTable] = [ + AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/0")!), + AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/8")!), + AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/9")!) + ] + + var layers = [AGSLayerContent]() + featureTables.forEach { (featureTable) in + layers.append(AGSFeatureLayer(featureTable: featureTable)) + } + + return layers + } + + /// Counts the number of layers in a map's operational layers and base map. + /// - Parameter map: The map to count the layers in. + func layerCount(map: AGSMap) -> Int { + return layerCount(operationalLayers: map.operationalLayers as! [AGSLayer], basemap: map.basemap) + } + + /// Counts the number of layers in a map's operational layers and base map. + /// - Parameter scene: The scene to count the layers in. + func layerCount(scene: AGSScene) -> Int { + return layerCount(operationalLayers: scene.operationalLayers as! [AGSLayer], basemap: scene.basemap) + } + + /// Counts the total number of layers in an operationalLayers array and basemap. + /// - Parameter operationalLayers: The operational layers to count. + /// - Parameter basemap: The base map containing the base and reference layers to count + func layerCount(operationalLayers: [AGSLayer], basemap: AGSBasemap?) -> Int { + var count = operationalLayers.count + if let refLayers = basemap?.referenceLayers { + count += refLayers.count + } + + if let baseLayers = basemap?.baseLayers { + count += baseLayers.count + } + + return count + } +} From 47e9fd9541408da300c302e44b77c820f3b5ea46 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Thu, 2 Jan 2020 14:04:35 -0800 Subject: [PATCH 02/47] Fix Auto Layout issues with MeasureToolbar The issues stemmed from the toolbar laying out its subviews before it had a valid size. The solution I used is to assign the toolbar's items in `layoutSubviews()`, which is only called after the toolbar is positioned in its super view. I also made the result view a bar item. --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 107 ++++++--------------- 1 file changed, 30 insertions(+), 77 deletions(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index baae9de5..7d330e16 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -37,6 +37,7 @@ class MeasureResultView: UIView { valueLabel.text = helpText unitButton.isHidden = true unitButton.setTitle(nil, for: .normal) + invalidateIntrinsicContentSize() } } } @@ -220,8 +221,6 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private var undoButton: UIBarButtonItem! private var redoButton: UIBarButtonItem! private var clearButton: UIBarButtonItem! - private var rightHiddenPlaceholderView: UIView! - private var leftHiddenPlaceholderView: UIView! private var segControl: UISegmentedControl! private var segControlItem: UIBarButtonItem! private var mode: MeasureToolbarMode? = nil { @@ -266,47 +265,33 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private func sharedInitialization() { let bundle = Bundle(for: type(of: self)) - let measureLengthImage = UIImage(named: "MeasureLength", in: bundle, compatibleWith: nil) - let measureAreaImage = UIImage(named: "MeasureArea", in: bundle, compatibleWith: nil) - let measureFeatureImage = UIImage(named: "MeasureFeature", in: bundle, compatibleWith: nil) - let undoImage = UIImage(named: "Undo", in: bundle, compatibleWith: nil) - let redoImage = UIImage(named: "Redo", in: bundle, compatibleWith: nil) - - undoButton = UIBarButtonItem(image: undoImage, style: .plain, target: nil, action: nil) - redoButton = UIBarButtonItem(image: redoImage, style: .plain, target: nil, action: nil) - clearButton = UIBarButtonItem(barButtonSystemItem: .trash, target: nil, action: nil) - - segControl = UISegmentedControl(items: ["Length", "Area", "Select"]) - segControl.setImage(measureLengthImage, forSegmentAt: 0) - segControl.setImage(measureAreaImage, forSegmentAt: 1) - segControl.setImage(measureFeatureImage, forSegmentAt: 2) + let measureLengthImage = UIImage(named: "MeasureLength", in: bundle, compatibleWith: traitCollection)! + let measureAreaImage = UIImage(named: "MeasureArea", in: bundle, compatibleWith: traitCollection)! + let measureFeatureImage = UIImage(named: "MeasureFeature", in: bundle, compatibleWith: traitCollection)! + let undoImage = UIImage(named: "Undo", in: bundle, compatibleWith: traitCollection) + let redoImage = UIImage(named: "Redo", in: bundle, compatibleWith: traitCollection) + + undoButton = UIBarButtonItem(image: undoImage, style: .plain, target: self, action: #selector(undoButtonTap)) + redoButton = UIBarButtonItem(image: redoImage, style: .plain, target: self, action: #selector(redoButtonTap)) + clearButton = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(clearButtonTap)) + + segControl = UISegmentedControl(items: [measureLengthImage, measureAreaImage, measureFeatureImage]) segControlItem = UIBarButtonItem(customView: segControl) resultView.buttonTapHandler = { [weak self] in self?.unitsButtonTap() } - undoButton.target = self - undoButton.action = #selector(undoButtonTap) - redoButton.target = self - redoButton.action = #selector(redoButtonTap) - clearButton.target = self - clearButton.action = #selector(clearButtonTap) - segControl.addTarget(self, action: #selector(segmentControlValueChanged), for: .valueChanged) - let flexButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - - rightHiddenPlaceholderView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1)) - let rightHiddenPlaceholderButton = UIBarButtonItem(customView: rightHiddenPlaceholderView) - rightHiddenPlaceholderButton.isEnabled = false - - leftHiddenPlaceholderView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1)) - let leftHiddenPlaceholderButton = UIBarButtonItem(customView: leftHiddenPlaceholderView) - leftHiddenPlaceholderButton.isEnabled = false - - sketchModeButtons = [segControlItem, leftHiddenPlaceholderButton, flexButton, rightHiddenPlaceholderButton, undoButton, redoButton, clearButton] - selectModeButtons = [segControlItem, leftHiddenPlaceholderButton, flexButton, rightHiddenPlaceholderButton] + let flexibleSpaceItem1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let resultViewItem = UIBarButtonItem(customView: resultView) + let flexibleSpaceItem2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + selectModeButtons = [segControlItem, + flexibleSpaceItem1, + resultViewItem, + flexibleSpaceItem2] + sketchModeButtons = selectModeButtons + [undoButton, redoButton, clearButton] // notification NotificationCenter.default.addObserver(self, selector: #selector(sketchEditorGeometryDidChange(_:)), name: .AGSSketchEditorGeometryDidChange, object: nil) @@ -341,46 +326,16 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } } - private var didSetConstraints: Bool = false - - override public func updateConstraints() { - super.updateConstraints() - - guard !didSetConstraints else { - return + override public func layoutSubviews() { + switch mode { + case .length, .area: + items = sketchModeButtons + case .feature: + items = selectModeButtons + case .none: + items = [] } - - // NOTE: Cannot add resultView as a subview until updateConstraints - // or else the constraints wont be setup correctly. - addSubview(resultView) - - resultView.translatesAutoresizingMaskIntoConstraints = false - - resultView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - - // The following constraints cause the results view to be centered, - // however if the content is too big it is allowed to grow to the right. - // The two centerX constraints are arranged with specific priorities to cause this. - - let space: CGFloat = 2 - - let c1 = resultView.leadingAnchor.constraint(greaterThanOrEqualTo: leftHiddenPlaceholderView.trailingAnchor, constant: space) - c1.priority = .required - - // have to give this just below required, otherwise before the left and right views are setup in - // their proper locations we can get constraint errors - let c2 = resultView.trailingAnchor.constraint(lessThanOrEqualTo: rightHiddenPlaceholderView.leadingAnchor, constant: -space) - c2.priority = UILayoutPriority(rawValue: 999) - - let c3 = NSLayoutConstraint(item: resultView, attribute: .centerX, relatedBy: .greaterThanOrEqual, toItem: self, attribute: .centerX, multiplier: 1, constant: 0) - c3.priority = .required - - let c4 = NSLayoutConstraint(item: resultView, attribute: .centerX, relatedBy: .lessThanOrEqual, toItem: self, attribute: .centerX, multiplier: 1, constant: 0) - c4.priority = .defaultLow - - NSLayoutConstraint.activate([c1, c2, c3, c4]) - - didSetConstraints = true + super.layoutSubviews() } override public class var requiresConstraintBasedLayout: Bool { @@ -396,6 +351,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } else if segControl.selectedSegmentIndex == 2 { startFeatureMode() } + setNeedsLayout() } private func startLineMode() { @@ -405,7 +361,6 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .length selectionOverlay?.isVisible = false - self.items = sketchModeButtons mapView?.sketchEditor = lineSketchEditor if !lineSketchEditor.isStarted { @@ -420,7 +375,6 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .area selectionOverlay?.isVisible = false - self.items = sketchModeButtons mapView?.sketchEditor = areaSketchEditor if !areaSketchEditor.isStarted { @@ -435,7 +389,6 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .feature selectionOverlay?.isVisible = true - self.items = selectModeButtons mapView?.sketchEditor = nil } From ed5bbae05338f336fb8e0c7c807659ce99681836 Mon Sep 17 00:00:00 2001 From: Philip Ridgeway Date: Thu, 2 Jan 2020 14:55:37 -0800 Subject: [PATCH 03/47] Fix Auto Layout issues with JobManagerExample The issue was the the toolbar was trying to layout its subviews when it had a width of zero. By assigning the items after the view has laid out its subviews, the toolbar has a valid width before trying to layout its subviews. --- .../JobManagerExample.swift | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 9516bbe9..27828481 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -129,19 +129,6 @@ class JobManagerExample: TableViewController { // now anchor toolbar below new safe area toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - // button to kick off a new job - let kickOffJobItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(kickOffJob)) - - // button to resume all paused jobs - // use this to resume the paused jobs you have after restarting your app - let resumeAllPausedJobsItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(resumeAllPausedJobs)) - - // button to clear the finished jobs - let clearFinishedJobsItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(clearFinishedJobs)) - - let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - toolbar.items = [kickOffJobItem, flex, resumeAllPausedJobsItem, flex, clearFinishedJobsItem] - // request authorization for user notifications, this way we can notify user in bg when job complete let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, _) in @@ -154,6 +141,25 @@ class JobManagerExample: TableViewController { tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if let toolbar = toolbar, toolbar.items == nil { + // button to kick off a new job + let kickOffJobItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(kickOffJob)) + + // button to resume all paused jobs + // use this to resume the paused jobs you have after restarting your app + let resumeAllPausedJobsItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(resumeAllPausedJobs)) + + // button to clear the finished jobs + let clearFinishedJobsItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(clearFinishedJobs)) + + let flexibleSpace1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let flexibleSpace2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.items = [kickOffJobItem, flexibleSpace1, resumeAllPausedJobsItem, flexibleSpace2, clearFinishedJobsItem] + } + } + @objc func resumeAllPausedJobs() { JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) From e79db7f9e0eaffda9efda87abd62b748ac0ef5ea Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 11:05:21 -0800 Subject: [PATCH 04/47] fix units selection bug when going from feature selection where a line was selected to measure area mode --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 46 ++++++++++++++++------ Toolkit/ArcGISToolkit/Scalebar.swift | 2 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index 7d330e16..847261d5 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -407,27 +407,49 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mapView?.sketchEditor?.clearGeometry() } + private var linearUnits: [AGSLinearUnit] { + let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] + return linearUnitIDs + .compactMap { AGSLinearUnit(unitID: $0) } + .sorted { $0.pluralDisplayName < $1.pluralDisplayName } + } + + private var areaUnits: [AGSAreaUnit] { + let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] + return areaUnitIDs + .compactMap { AGSAreaUnit(unitID: $0) } + .sorted { $0.pluralDisplayName < $1.pluralDisplayName } + } + private func unitsButtonTap() { let units: [AGSUnit] let selectedUnit: AGSUnit - if mapView?.sketchEditor == lineSketchEditor || - selectedGeometry?.geometryType == .polyline { - let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] - units = linearUnitIDs.compactMap { AGSLinearUnit(unitID: $0) } + + guard let mode = mode else { return } + + switch mode { + case .length: + units = linearUnits selectedUnit = selectedLinearUnit - } else if mapView?.sketchEditor == areaSketchEditor || - selectedGeometry?.geometryType == .envelope || - selectedGeometry?.geometryType == .polygon { - let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] - units = areaUnitIDs.compactMap { AGSAreaUnit(unitID: $0) } + case .area: + units = areaUnits selectedUnit = selectedAreaUnit - } else { - return + case .feature: + if selectedGeometry?.geometryType == .polyline { + units = linearUnits + selectedUnit = selectedLinearUnit + } else if selectedGeometry?.geometryType == .envelope || + selectedGeometry?.geometryType == .polygon { + units = areaUnits + selectedUnit = selectedAreaUnit + } else { + return + } } let unitsViewController = UnitsViewController() unitsViewController.delegate = self - unitsViewController.units = units.sorted { $0.pluralDisplayName < $1.pluralDisplayName } + unitsViewController.units = units unitsViewController.selectedUnit = selectedUnit let navigationController = UINavigationController(rootViewController: unitsViewController) diff --git a/Toolkit/ArcGISToolkit/Scalebar.swift b/Toolkit/ArcGISToolkit/Scalebar.swift index 092669ee..faf76e41 100644 --- a/Toolkit/ArcGISToolkit/Scalebar.swift +++ b/Toolkit/ArcGISToolkit/Scalebar.swift @@ -181,7 +181,7 @@ public class Scalebar: UIView { } } - @IBInspectable public var lineColor: UIColor = UIColor.white { + @IBInspectable public var lineColor: UIColor = .white { didSet { setNeedsDisplay() } From 159c4b173c81fe0d7d08a474a11dd8b7c1ba2524 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 11:11:44 -0800 Subject: [PATCH 05/47] fix issue where calculations were incorrect --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index 847261d5..05a25781 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -359,13 +359,16 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } - mode = .length selectionOverlay?.isVisible = false mapView?.sketchEditor = lineSketchEditor if !lineSketchEditor.isStarted { lineSketchEditor.start(with: AGSSketchCreationMode.polyline) } + + // update mode after sketch editor is updated, + // otherwise calculations will be incorrect + mode = .length } private func startAreaMode() { @@ -373,13 +376,16 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } - mode = .area selectionOverlay?.isVisible = false mapView?.sketchEditor = areaSketchEditor if !areaSketchEditor.isStarted { areaSketchEditor.start(with: AGSSketchCreationMode.polygon) } + + // update mode after sketch editor is updated, + // otherwise calculations will be incorrect + mode = .area } private func startFeatureMode() { @@ -387,9 +393,9 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } - mode = .feature selectionOverlay?.isVisible = true mapView?.sketchEditor = nil + mode = .feature } @objc From 5aa2db4950388af4bc11ca0c689618163579d4b4 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 11:34:15 -0800 Subject: [PATCH 06/47] pr feedback --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 39 +++++++++++----------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index 05a25781..9c90968e 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -223,12 +223,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private var clearButton: UIBarButtonItem! private var segControl: UISegmentedControl! private var segControlItem: UIBarButtonItem! - private var mode: MeasureToolbarMode? = nil { - didSet { - guard mode != oldValue else { return } - updateMeasurement() - } - } + private var mode: MeasureToolbarMode? private let geodeticCurveType: AGSGeodeticCurveType = .geodesic // This is the threshold for which when the planar measurements are above, @@ -359,6 +354,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } + mode = .length selectionOverlay?.isVisible = false mapView?.sketchEditor = lineSketchEditor @@ -366,9 +362,9 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { lineSketchEditor.start(with: AGSSketchCreationMode.polyline) } - // update mode after sketch editor is updated, - // otherwise calculations will be incorrect - mode = .length + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } private func startAreaMode() { @@ -376,6 +372,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } + mode = .area selectionOverlay?.isVisible = false mapView?.sketchEditor = areaSketchEditor @@ -383,9 +380,9 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { areaSketchEditor.start(with: AGSSketchCreationMode.polygon) } - // update mode after sketch editor is updated, - // otherwise calculations will be incorrect - mode = .area + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } private func startFeatureMode() { @@ -393,9 +390,13 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { return } + mode = .feature selectionOverlay?.isVisible = true mapView?.sketchEditor = nil - mode = .feature + + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } @objc @@ -413,19 +414,19 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mapView?.sketchEditor?.clearGeometry() } - private var linearUnits: [AGSLinearUnit] { + private lazy var linearUnits: [AGSLinearUnit] = { let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] return linearUnitIDs - .compactMap { AGSLinearUnit(unitID: $0) } + .compactMap (AGSLinearUnit.init) .sorted { $0.pluralDisplayName < $1.pluralDisplayName } - } + }() - private var areaUnits: [AGSAreaUnit] { + private lazy var areaUnits: [AGSAreaUnit] = { let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] return areaUnitIDs - .compactMap { AGSAreaUnit(unitID: $0) } + .compactMap (AGSAreaUnit.init) .sorted { $0.pluralDisplayName < $1.pluralDisplayName } - } + }() private func unitsButtonTap() { let units: [AGSUnit] From 66e1a8a4ddf257950812559d6074b559682fa7dd Mon Sep 17 00:00:00 2001 From: R Olson Date: Fri, 3 Jan 2020 11:47:38 -0800 Subject: [PATCH 07/47] Update Toolkit/ArcGISToolkit/MeasureToolbar.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index 9c90968e..deaba23f 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -417,7 +417,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private lazy var linearUnits: [AGSLinearUnit] = { let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] return linearUnitIDs - .compactMap (AGSLinearUnit.init) + .compactMap(AGSLinearUnit.init) .sorted { $0.pluralDisplayName < $1.pluralDisplayName } }() From 2fa18016aa8364ac8c5d51191db4758bf65b149f Mon Sep 17 00:00:00 2001 From: R Olson Date: Fri, 3 Jan 2020 11:47:43 -0800 Subject: [PATCH 08/47] Update Toolkit/ArcGISToolkit/MeasureToolbar.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/MeasureToolbar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index deaba23f..3b07d4f1 100644 --- a/Toolkit/ArcGISToolkit/MeasureToolbar.swift +++ b/Toolkit/ArcGISToolkit/MeasureToolbar.swift @@ -424,7 +424,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { private lazy var areaUnits: [AGSAreaUnit] = { let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] return areaUnitIDs - .compactMap (AGSAreaUnit.init) + .compactMap(AGSAreaUnit.init) .sorted { $0.pluralDisplayName < $1.pluralDisplayName } }() From 84b0c9b2bb0f765c23adb19a9b23f6bd82671c6f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 13:16:58 -0800 Subject: [PATCH 09/47] remove resume button for job manager. The button is confusing. We should just do this in viewDidLoad. --- .../JobManagerExample.swift | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 27828481..2ef062a0 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -139,6 +139,9 @@ class JobManagerExample: TableViewController { // job cell registration tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") + + // resume any paused jobs + JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) } override func viewDidLayoutSubviews() { @@ -147,24 +150,14 @@ class JobManagerExample: TableViewController { // button to kick off a new job let kickOffJobItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(kickOffJob)) - // button to resume all paused jobs - // use this to resume the paused jobs you have after restarting your app - let resumeAllPausedJobsItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(resumeAllPausedJobs)) - // button to clear the finished jobs let clearFinishedJobsItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(clearFinishedJobs)) - let flexibleSpace1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let flexibleSpace2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - toolbar.items = [kickOffJobItem, flexibleSpace1, resumeAllPausedJobsItem, flexibleSpace2, clearFinishedJobsItem] + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.items = [kickOffJobItem, flexibleSpace, clearFinishedJobsItem] } } - @objc - func resumeAllPausedJobs() { - JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) - } - @objc func clearFinishedJobs() { JobManager.shared.clearFinishedJobs() From ab7d076912b3fd4467e54204ebdca30bdd0d3cd1 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 14:40:39 -0800 Subject: [PATCH 10/47] adds support for iOS 13 background tasks framework --- .../ArcGISToolkitExamples/AppDelegate.swift | 9 ++++ Examples/ArcGISToolkitExamples/Info.plist | 4 ++ Toolkit/ArcGISToolkit/JobManager.swift | 49 +++++++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 9dce979a..98114965 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -20,6 +20,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Override point for customization after application launch. + + if #available(iOS 13.0, *) { + let result = JobManager.shared.registerForBackgroundUpdates() + print("background registration result: \(result)") + } + return true } @@ -48,6 +54,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Here is where we forward background fetch to the JobManager // so that jobs can be updated in the background func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + // We only do it this way for pre-iOS 13. Otherwise we use BGTask above. + // This method doesn't get called for iOS 13 or later because we have an entry + // in the plist for BGTaskSchedulerPermittedIdentifiers. JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) } } diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 39f45da8..1ebb93dd 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.esri.arcgis.toolkit.jobmanager.refresh + CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index c4384d12..578318da 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -13,6 +13,7 @@ import ArcGIS import Foundation +import BackgroundTasks internal typealias JSONDictionary = [String: Any] public typealias JobStatusHandler = (AGSJobStatus) -> Void @@ -62,14 +63,14 @@ public class JobManager: NSObject { } didSet { keyedJobs.values.forEach { observeJobStatus(job: $0) } - + // If there was a change, then re-store the serialized AGSJobs in UserDefaults if keyedJobs != oldValue { saveJobsToUserDefaults() } } } - + /// A convenience accessor to the `AGSJob`s that the `JobManager` is managing. public var jobs: [AGSJob] { return Array(keyedJobs.values) @@ -112,7 +113,7 @@ public class JobManager: NSObject { jobStatusObservations.removeValue(forKey: job.serverJobID) } } - + /// Register an `AGSJob` with the `JobManager`. /// /// - Parameter job: The AGSJob to register. @@ -123,7 +124,7 @@ public class JobManager: NSObject { keyedJobs[jobUniqueID] = job return jobUniqueID } - + /// Unregister an `AGSJob` from the `JobManager`. /// /// - Parameter job: The job to unregister. @@ -198,6 +199,7 @@ public class JobManager: NSObject { /// - Parameters: /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) + @available(*, deprecated, message: "Please use `registerBackgroundTask` instead") public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if keyedJobs.isEmpty { return completionHandler(.noData) @@ -212,6 +214,45 @@ public class JobManager: NSObject { } } + private let bgTaskIdentifier = "com.esri.arcgis.toolkit.jobmanager.refresh" + + /// Registers a task for updating job status in the background. + /// You must add an entry in the app plist for com.esri.arcgis.toolkit.jobmanager.refresh under BGTaskSchedulerPermittedIdentifiers. + /// This must be called before the end of the app launch sequence. This must be tested on device, does not work on the simulator. + @available(iOS 13.0, *) + @discardableResult + public func registerForBackgroundUpdates() -> Bool { + return BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil, launchHandler: handleBackgroundRefresh) + } + + @available(iOS 13.0, *) + private func handleBackgroundRefresh(task: BGTask) { + print("handleBackgroundRefresh") + guard let task = task as? BGAppRefreshTask else { return } + + // Schedule next + scheduleNextBackgroundRefresh() + + JobManager.shared.checkStatusForAllJobs { success in + print("status check done: \(success)") + task.setTaskCompleted(success: success) + } + } + + @available(iOS 13.0, *) + private func scheduleNextBackgroundRefresh() { + print("scheduleNextBackgroundRefresh") + let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) + // Fetch no earlier than 15 seconds from now + request.earliestBeginDate = Date(timeIntervalSinceNow: 15) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + print("Could not schedule app refresh: \(error)") + } + } + /// Resume all paused and not-started `AGSJob`s. /// /// 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. From 1968a7de765a4574caa1778473ccd43483266d2c Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 15:18:32 -0800 Subject: [PATCH 11/47] adds support for iOS 13 background tasks framework --- .../project.pbxproj | 5 ++-- .../ArcGISToolkitExamples/AppDelegate.swift | 2 +- Toolkit/ArcGISToolkit/JobManager.swift | 26 ++++++++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 3ba84b0b..faa9534b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; + DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1110; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -508,7 +509,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -528,7 +529,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 98114965..73163fa3 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if #available(iOS 13.0, *) { let result = JobManager.shared.registerForBackgroundUpdates() - print("background registration result: \(result)") + print("-- background registration result: \(result)") } return true diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 578318da..e18df61b 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -93,6 +93,9 @@ public class JobManager: NSObject { deinit { keyedJobs.values.forEach { unObserveJobStatus(job: $0) } + if let observer = bgObserver { + NotificationCenter.default.removeObserver(observer) + } } private func toJSON() -> JSONDictionary { @@ -199,7 +202,7 @@ public class JobManager: NSObject { /// - Parameters: /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) - @available(*, deprecated, message: "Please use `registerBackgroundTask` instead") + @available(*, deprecated, message: "Please use `registerForBackgroundUpdates` instead") public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if keyedJobs.isEmpty { return completionHandler(.noData) @@ -215,6 +218,7 @@ public class JobManager: NSObject { } private let bgTaskIdentifier = "com.esri.arcgis.toolkit.jobmanager.refresh" + private var bgObserver: AnyObject? /// Registers a task for updating job status in the background. /// You must add an entry in the app plist for com.esri.arcgis.toolkit.jobmanager.refresh under BGTaskSchedulerPermittedIdentifiers. @@ -222,26 +226,36 @@ public class JobManager: NSObject { @available(iOS 13.0, *) @discardableResult public func registerForBackgroundUpdates() -> Bool { + // schedule a notification when application enters background + bgObserver = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in + self?.scheduleNextBackgroundRefresh() + } + // register the bg task return BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil, launchHandler: handleBackgroundRefresh) } @available(iOS 13.0, *) private func handleBackgroundRefresh(task: BGTask) { - print("handleBackgroundRefresh") + print("-- handleBackgroundRefresh") guard let task = task as? BGAppRefreshTask else { return } // Schedule next scheduleNextBackgroundRefresh() - JobManager.shared.checkStatusForAllJobs { success in - print("status check done: \(success)") + // check job status + let operation = JobManager.shared.checkStatusForAllJobs { success in + print("-- status check done: \(success)") task.setTaskCompleted(success: success) } + + task.expirationHandler = { + operation.cancel() + } } @available(iOS 13.0, *) private func scheduleNextBackgroundRefresh() { - print("scheduleNextBackgroundRefresh") + print("-- scheduleNextBackgroundRefresh") let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) // Fetch no earlier than 15 seconds from now request.earliestBeginDate = Date(timeIntervalSinceNow: 15) @@ -249,7 +263,7 @@ public class JobManager: NSObject { do { try BGTaskScheduler.shared.submit(request) } catch { - print("Could not schedule app refresh: \(error)") + print("-- Could not schedule app refresh: \(error)") } } From 1c409a5ed9063d37ee71ea0e84fd9d5d68bb3ee9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:12:46 -0800 Subject: [PATCH 12/47] fix retain cycles --- .../JobManagerExample.swift | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 2ef062a0..3cc44e08 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -141,7 +141,14 @@ class JobManagerExample: TableViewController { tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") // resume any paused jobs - JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) + JobManager.shared.resumeAllPausedJobs( + statusHandler: { [weak self] in + self?.jobStatusHandler(status: $0) + }, + completion: { [weak self] in + self?.jobCompletionHandler(result: $0, error: $1) + } + ) } override func viewDidLayoutSubviews() { @@ -270,13 +277,24 @@ class JobManagerExample: TableViewController { JobManager.shared.register(job: job) // start the job - job.start(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) + job.start( + statusHandler: { [weak self] in + self?.jobStatusHandler(status: $0) + }, + completion: { [weak self] in + self?.jobCompletionHandler(result: $0, error: $1) + } + ) // refresh the tableview self.tableView.reloadData() } } + deinit { + print("-- job manager example deinit") + } + func takeOffline(map: AGSMap, extent: AGSEnvelope) { let task = AGSOfflineMapTask(onlineMap: map) @@ -296,7 +314,14 @@ class JobManagerExample: TableViewController { JobManager.shared.register(job: job) // start the job - job.start(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) + job.start( + statusHandler: { [weak self] in + self?.jobStatusHandler(status: $0) + }, + completion: { [weak self] in + self?.jobCompletionHandler(result: $0, error: $1) + } + ) // refresh the tableview self.tableView.reloadData() From f9be230413c34a87392f2c3ba226a4d0932dfea4 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:13:56 -0800 Subject: [PATCH 13/47] remove deinit --- Examples/ArcGISToolkitExamples/JobManagerExample.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 3cc44e08..fc994e5f 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -291,10 +291,6 @@ class JobManagerExample: TableViewController { } } - deinit { - print("-- job manager example deinit") - } - func takeOffline(map: AGSMap, extent: AGSEnvelope) { let task = AGSOfflineMapTask(onlineMap: map) From 90a7e4f7e28ddcb15c83ce49210b6f56998bc969 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:28:49 -0800 Subject: [PATCH 14/47] adds support for iOS 13 background tasks framework --- Toolkit/ArcGISToolkit/JobManager.swift | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index e18df61b..464fe0d4 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -226,18 +226,24 @@ public class JobManager: NSObject { @available(iOS 13.0, *) @discardableResult public func registerForBackgroundUpdates() -> Bool { + // register the bg task + let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { task in + self.handleBackgroundRefresh(task: task as! BGAppRefreshTask) + } + // schedule a notification when application enters background - bgObserver = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in - self?.scheduleNextBackgroundRefresh() + if bgObserver == nil { + bgObserver = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in + self?.scheduleNextBackgroundRefresh() + } } - // register the bg task - return BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil, launchHandler: handleBackgroundRefresh) + + return result } @available(iOS 13.0, *) - private func handleBackgroundRefresh(task: BGTask) { + private func handleBackgroundRefresh(task: BGAppRefreshTask) { print("-- handleBackgroundRefresh") - guard let task = task as? BGAppRefreshTask else { return } // Schedule next scheduleNextBackgroundRefresh() @@ -258,7 +264,7 @@ public class JobManager: NSObject { print("-- scheduleNextBackgroundRefresh") let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) // Fetch no earlier than 15 seconds from now - request.earliestBeginDate = Date(timeIntervalSinceNow: 15) + request.earliestBeginDate = Date(timeIntervalSinceNow: 60) do { try BGTaskScheduler.shared.submit(request) From de934aea995d0588cb07663174e2751b9b59c22d Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:37:42 -0800 Subject: [PATCH 15/47] adds support for iOS 13 background tasks framework --- Toolkit/ArcGISToolkit/JobManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 464fe0d4..929d2c98 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -222,7 +222,8 @@ public class JobManager: NSObject { /// Registers a task for updating job status in the background. /// You must add an entry in the app plist for com.esri.arcgis.toolkit.jobmanager.refresh under BGTaskSchedulerPermittedIdentifiers. - /// This must be called before the end of the app launch sequence. This must be tested on device, does not work on the simulator. + /// This must be called before the end of the app launch sequence. + /// This must be tested on device, does not work on the simulator. For debugging this, see documentation here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development?language=objc @available(iOS 13.0, *) @discardableResult public func registerForBackgroundUpdates() -> Bool { From de79ca7d1f1f92f844de3e7afbf6f4a898a08758 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:39:45 -0800 Subject: [PATCH 16/47] adds support for iOS 13 background tasks framework --- Examples/ArcGISToolkitExamples/AppDelegate.swift | 3 +-- Toolkit/ArcGISToolkit/JobManager.swift | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 73163fa3..2c0ef216 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -22,8 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. if #available(iOS 13.0, *) { - let result = JobManager.shared.registerForBackgroundUpdates() - print("-- background registration result: \(result)") + JobManager.shared.registerForBackgroundUpdates() } return true diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 929d2c98..4fdf3d39 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -244,14 +244,11 @@ public class JobManager: NSObject { @available(iOS 13.0, *) private func handleBackgroundRefresh(task: BGAppRefreshTask) { - print("-- handleBackgroundRefresh") - - // Schedule next + // Schedule next refresh scheduleNextBackgroundRefresh() // check job status let operation = JobManager.shared.checkStatusForAllJobs { success in - print("-- status check done: \(success)") task.setTaskCompleted(success: success) } @@ -262,7 +259,6 @@ public class JobManager: NSObject { @available(iOS 13.0, *) private func scheduleNextBackgroundRefresh() { - print("-- scheduleNextBackgroundRefresh") let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) // Fetch no earlier than 15 seconds from now request.earliestBeginDate = Date(timeIntervalSinceNow: 60) @@ -270,7 +266,7 @@ public class JobManager: NSObject { do { try BGTaskScheduler.shared.submit(request) } catch { - print("-- Could not schedule app refresh: \(error)") + print("Could not schedule app refresh: \(error)") } } From 7d46485640e8d4190f6552ddb5ee81b6e1613c03 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Fri, 3 Jan 2020 16:49:06 -0800 Subject: [PATCH 17/47] adds support for iOS 13 background tasks framework --- Toolkit/ArcGISToolkit/JobManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 4fdf3d39..a5fe7d77 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -260,7 +260,6 @@ public class JobManager: NSObject { @available(iOS 13.0, *) private func scheduleNextBackgroundRefresh() { let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) - // Fetch no earlier than 15 seconds from now request.earliestBeginDate = Date(timeIntervalSinceNow: 60) do { From 0081c8a7fa1c144c41ba4d95aef155eb1c8df549 Mon Sep 17 00:00:00 2001 From: R Olson Date: Mon, 6 Jan 2020 08:02:25 -0800 Subject: [PATCH 18/47] Update Toolkit/ArcGISToolkit/JobManager.swift Co-Authored-By: Philip Ridgeway --- Toolkit/ArcGISToolkit/JobManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index a5fe7d77..08579229 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -202,7 +202,7 @@ public class JobManager: NSObject { /// - Parameters: /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) - @available(*, deprecated, message: "Please use `registerForBackgroundUpdates` instead") + @available(iOS, deprecated: 13.0, message: "Please use 'registerForBackgroundUpdates()' instead") public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if keyedJobs.isEmpty { return completionHandler(.noData) From fa0f694626cba0ac0199111bd889e5802dac4149 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 08:11:24 -0800 Subject: [PATCH 19/47] pr feedback --- Toolkit/ArcGISToolkit/JobManager.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index a5fe7d77..e59dab7f 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -224,12 +224,14 @@ public class JobManager: NSObject { /// You must add an entry in the app plist for com.esri.arcgis.toolkit.jobmanager.refresh under BGTaskSchedulerPermittedIdentifiers. /// This must be called before the end of the app launch sequence. /// This must be tested on device, does not work on the simulator. For debugging this, see documentation here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development?language=objc + /// Returns whether or not the registration was successful. If the registration was not successful, please check your application's plist for the appropriately entry + /// as mentioned above. @available(iOS 13.0, *) @discardableResult public func registerForBackgroundUpdates() -> Bool { // register the bg task - let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { task in - self.handleBackgroundRefresh(task: task as! BGAppRefreshTask) + let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { [weak self] task in + self?.handleBackgroundRefresh(task: task as! BGAppRefreshTask) } // schedule a notification when application enters background From b1fc66efa8fb8f052337f4dbf3e018900dbc6ce7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 08:17:52 -0800 Subject: [PATCH 20/47] remove dev team info --- Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index faa9534b..3ba84b0b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -230,7 +230,6 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1110; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -509,7 +508,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -529,7 +528,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", From 6eca56a7b1ad9aac1ce153d32a5e4da36b1627df Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 09:46:35 -0800 Subject: [PATCH 21/47] pause jobs in dealloc --- .../ArcGISToolkitExamples/JobManagerExample.swift | 12 ++++++++++++ Toolkit/ArcGISToolkit/JobManager.swift | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index fc994e5f..54c29f25 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -151,6 +151,18 @@ class JobManagerExample: TableViewController { ) } + deinit { + // When we are deinited we pause all running jobs. + // In a normal app you would not need to do this, but this view controller + // is acting as an app example. Thus when it goes out of scope, we pause + // the jobs so that when the view controller is re-shown we can resume and rewire + // the handlers up to them. Otherwise we would have no way to hook into the status + // of any currently running jobs. A normal app would not likely need this as it would + // have an object globally wiring up status and completion handlers to jobs. + // But since this sample view controller can be pushed/pop, we need this. + JobManager.shared.pauseAllJobs() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if let toolbar = toolbar, toolbar.items == nil { diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 4cd3df05..593edd78 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -287,6 +287,13 @@ public class JobManager: NSObject { } } + /// Pauses any currently running job. + public func pauseAllJobs() { + keyedJobs.lazy.filter { $0.value.status == AGSJobStatus.started }.forEach { + $0.value.progress.pause() + } + } + /// Saves all managed `AGSJob`s to User Defaults. /// /// This happens automatically when the `AGSJob`s are registered/unregistered. From 1158b277548d2b17e1c9b02cf6c069cdb9d1226f Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 10:29:19 -0800 Subject: [PATCH 22/47] use viewDidAppear and viewWillDisappear instead of viewDidLoad and deinit --- .../JobManagerExample.swift | 16 +++++++++++----- Toolkit/ArcGISToolkit/JobManager.swift | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 54c29f25..002a9823 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -139,8 +139,10 @@ class JobManagerExample: TableViewController { // job cell registration tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") - - // resume any paused jobs + } + + override func viewDidAppear(_ animated: Bool) { + // resume any paused jobs when this view controller is shown JobManager.shared.resumeAllPausedJobs( statusHandler: { [weak self] in self?.jobStatusHandler(status: $0) @@ -149,18 +151,22 @@ class JobManagerExample: TableViewController { self?.jobCompletionHandler(result: $0, error: $1) } ) + + super.viewDidAppear(animated) } - deinit { - // When we are deinited we pause all running jobs. + override func viewWillDisappear(_ animated: Bool) { + // When the view controller is popped, we pause all running jobs. // In a normal app you would not need to do this, but this view controller - // is acting as an app example. Thus when it goes out of scope, we pause + // is acting as an app example. Thus when it is not being shown, we pause // the jobs so that when the view controller is re-shown we can resume and rewire // the handlers up to them. Otherwise we would have no way to hook into the status // of any currently running jobs. A normal app would not likely need this as it would // have an object globally wiring up status and completion handlers to jobs. // But since this sample view controller can be pushed/pop, we need this. JobManager.shared.pauseAllJobs() + + super.viewWillDisappear(animated) } override func viewDidLayoutSubviews() { diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 593edd78..1f473062 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -262,7 +262,7 @@ public class JobManager: NSObject { @available(iOS 13.0, *) private func scheduleNextBackgroundRefresh() { let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: 60) + request.earliestBeginDate = Date(timeIntervalSinceNow: 30) do { try BGTaskScheduler.shared.submit(request) From e6dad14cb3fe151bb20ea1507ac9d838aafe56f2 Mon Sep 17 00:00:00 2001 From: R Olson Date: Mon, 6 Jan 2020 11:15:43 -0800 Subject: [PATCH 23/47] Update Examples/ArcGISToolkitExamples/AppDelegate.swift Co-Authored-By: Philip Ridgeway --- Examples/ArcGISToolkitExamples/AppDelegate.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 2c0ef216..a50ea5d0 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -56,6 +56,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // We only do it this way for pre-iOS 13. Otherwise we use BGTask above. // This method doesn't get called for iOS 13 or later because we have an entry // in the plist for BGTaskSchedulerPermittedIdentifiers. - JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) + if #available(iOS 13.0, *) { + // Nothing to do here. + } else { + JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) + } } } From 89135d82202c2c4b5f6d4ddfa2522f658cb63173 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 11:19:38 -0800 Subject: [PATCH 24/47] pr feedback --- Toolkit/ArcGISToolkit/JobManager.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 1f473062..3cf87c3e 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -289,8 +289,9 @@ public class JobManager: NSObject { /// Pauses any currently running job. public func pauseAllJobs() { - keyedJobs.lazy.filter { $0.value.status == AGSJobStatus.started }.forEach { - $0.value.progress.pause() + keyedJobs.values.forEach { + guard $0.status == .started else { return } + $0.progress.pause() } } From f8abbcf7bbd5858d3fc60ad036dd9c14e888c412 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 16:05:33 -0800 Subject: [PATCH 25/47] use background tasks to keep app alive --- .../project.pbxproj | 5 ++-- .../ArcGISToolkitExamples/AppDelegate.swift | 5 ---- .../JobManagerExample.swift | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 3ba84b0b..faa9534b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -230,6 +230,7 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; + DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1110; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -508,7 +509,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -528,7 +529,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = P8HGHS7JQ8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index a50ea5d0..69ac98b8 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -20,11 +20,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // Override point for customization after application launch. - - if #available(iOS 13.0, *) { - JobManager.shared.registerForBackgroundUpdates() - } - return true } diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 002a9823..acaf5c29 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -108,6 +108,8 @@ class JobManagerExample: TableViewController { return JobManager.shared.jobs } + var backgroundTaskIdentifiers = Set() + var toolbar: UIToolbar? override func viewDidLoad() { @@ -166,6 +168,10 @@ class JobManagerExample: TableViewController { // But since this sample view controller can be pushed/pop, we need this. JobManager.shared.pauseAllJobs() + // clear out background tasks that we started for the jobs + backgroundTaskIdentifiers.forEach { UIApplication.shared.endBackgroundTask($0) } + backgroundTaskIdentifiers.removeAll() + super.viewWillDisappear(animated) } @@ -293,6 +299,9 @@ class JobManagerExample: TableViewController { // register the job with our JobManager shared instance JobManager.shared.register(job: job) + + // try to keep the app running in the background for this job if possible + let backgroundTaskIdentifier = self.startBackgroundTask() // start the job job.start( @@ -300,6 +309,7 @@ class JobManagerExample: TableViewController { self?.jobStatusHandler(status: $0) }, completion: { [weak self] in + self?.endBackgroundTask(backgroundTaskIdentifier) self?.jobCompletionHandler(result: $0, error: $1) } ) @@ -309,6 +319,20 @@ class JobManagerExample: TableViewController { } } + func startBackgroundTask() -> UIBackgroundTaskIdentifier { + var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + backgroundTaskIdentifiers.insert(backgroundTaskIdentifier) + return backgroundTaskIdentifier + } + + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { + UIApplication.shared.endBackgroundTask(identifier) + backgroundTaskIdentifiers.remove(identifier) + } + func takeOffline(map: AGSMap, extent: AGSEnvelope) { let task = AGSOfflineMapTask(onlineMap: map) @@ -326,6 +350,9 @@ class JobManagerExample: TableViewController { // register the job with our JobManager shared instance JobManager.shared.register(job: job) + + // try to keep the app running in the background for this job if possible + let backgroundTaskIdentifier = self.startBackgroundTask() // start the job job.start( @@ -333,6 +360,7 @@ class JobManagerExample: TableViewController { self?.jobStatusHandler(status: $0) }, completion: { [weak self] in + self?.endBackgroundTask(backgroundTaskIdentifier) self?.jobCompletionHandler(result: $0, error: $1) } ) From 10a001b1c8721d492b7658f0b5a6ff3abbab52b9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 16:09:13 -0800 Subject: [PATCH 26/47] remove new iOS 13 BackgroundTasks code as it was in-effective --- .../ArcGISToolkitExamples/AppDelegate.swift | 13 ---- Examples/ArcGISToolkitExamples/Info.plist | 4 -- Toolkit/ArcGISToolkit/JobManager.swift | 60 +------------------ 3 files changed, 1 insertion(+), 76 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 69ac98b8..baf37d18 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -44,17 +44,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - - // Here is where we forward background fetch to the JobManager - // so that jobs can be updated in the background - func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // We only do it this way for pre-iOS 13. Otherwise we use BGTask above. - // This method doesn't get called for iOS 13 or later because we have an entry - // in the plist for BGTaskSchedulerPermittedIdentifiers. - if #available(iOS 13.0, *) { - // Nothing to do here. - } else { - JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) - } - } } diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 1ebb93dd..39f45da8 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -2,10 +2,6 @@ - BGTaskSchedulerPermittedIdentifiers - - com.esri.arcgis.toolkit.jobmanager.refresh - CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index 3cf87c3e..9f073926 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -13,7 +13,6 @@ import ArcGIS import Foundation -import BackgroundTasks internal typealias JSONDictionary = [String: Any] public typealias JobStatusHandler = (AGSJobStatus) -> Void @@ -93,9 +92,6 @@ public class JobManager: NSObject { deinit { keyedJobs.values.forEach { unObserveJobStatus(job: $0) } - if let observer = bgObserver { - NotificationCenter.default.removeObserver(observer) - } } private func toJSON() -> JSONDictionary { @@ -202,7 +198,7 @@ public class JobManager: NSObject { /// - Parameters: /// - application: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) /// - completionHandler: See [Apple's documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) - @available(iOS, deprecated: 13.0, message: "Please use 'registerForBackgroundUpdates()' instead") + @available(iOS, deprecated: 13.0, message: "Please use 'UIApplication.shared.beginBackgroundTask(expirationHandler:)' when kicking off your job instead") public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if keyedJobs.isEmpty { return completionHandler(.noData) @@ -217,60 +213,6 @@ public class JobManager: NSObject { } } - private let bgTaskIdentifier = "com.esri.arcgis.toolkit.jobmanager.refresh" - private var bgObserver: AnyObject? - - /// Registers a task for updating job status in the background. - /// You must add an entry in the app plist for com.esri.arcgis.toolkit.jobmanager.refresh under BGTaskSchedulerPermittedIdentifiers. - /// This must be called before the end of the app launch sequence. - /// This must be tested on device, does not work on the simulator. For debugging this, see documentation here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development?language=objc - /// Returns whether or not the registration was successful. If the registration was not successful, please check your application's plist for the appropriately entry - /// as mentioned above. - @available(iOS 13.0, *) - @discardableResult - public func registerForBackgroundUpdates() -> Bool { - // register the bg task - let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { [weak self] task in - self?.handleBackgroundRefresh(task: task as! BGAppRefreshTask) - } - - // schedule a notification when application enters background - if bgObserver == nil { - bgObserver = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in - self?.scheduleNextBackgroundRefresh() - } - } - - return result - } - - @available(iOS 13.0, *) - private func handleBackgroundRefresh(task: BGAppRefreshTask) { - // Schedule next refresh - scheduleNextBackgroundRefresh() - - // check job status - let operation = JobManager.shared.checkStatusForAllJobs { success in - task.setTaskCompleted(success: success) - } - - task.expirationHandler = { - operation.cancel() - } - } - - @available(iOS 13.0, *) - private func scheduleNextBackgroundRefresh() { - let request = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: 30) - - do { - try BGTaskScheduler.shared.submit(request) - } catch { - print("Could not schedule app refresh: \(error)") - } - } - /// Resume all paused and not-started `AGSJob`s. /// /// 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. From 78cd3f0f7f93aeb5188b6c4d244313a2c9b02abe Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Mon, 6 Jan 2020 16:15:52 -0800 Subject: [PATCH 27/47] remove team information --- Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index faa9534b..3ba84b0b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -230,7 +230,6 @@ TargetAttributes = { 8839043A1DF6022A001F3188 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = P8HGHS7JQ8; LastSwiftMigration = 1110; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -509,7 +508,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", @@ -529,7 +528,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - DEVELOPMENT_TEAM = P8HGHS7JQ8; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", From 55278f6843a5eadefb056d2bef55bf7b64c06261 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 13:21:24 -0800 Subject: [PATCH 28/47] move endBackgroundTask to later --- Examples/ArcGISToolkitExamples/JobManagerExample.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index acaf5c29..5625be4c 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -309,8 +309,8 @@ class JobManagerExample: TableViewController { self?.jobStatusHandler(status: $0) }, completion: { [weak self] in - self?.endBackgroundTask(backgroundTaskIdentifier) self?.jobCompletionHandler(result: $0, error: $1) + self?.endBackgroundTask(backgroundTaskIdentifier) } ) @@ -360,8 +360,8 @@ class JobManagerExample: TableViewController { self?.jobStatusHandler(status: $0) }, completion: { [weak self] in - self?.endBackgroundTask(backgroundTaskIdentifier) self?.jobCompletionHandler(result: $0, error: $1) + self?.endBackgroundTask(backgroundTaskIdentifier) } ) From 923b2909893308b22b0ab422d3efdedca2886c86 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 14:29:56 -0800 Subject: [PATCH 29/47] pr feedback --- .../JobManagerExample.swift | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 5625be4c..79002600 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -241,6 +241,9 @@ class JobManagerExample: TableViewController { } func generateGDB(URL: URL, syncModel: AGSSyncModel, extent: AGSEnvelope?) { + // try to keep the app running in the background for this job if possible + let backgroundTaskIdentifier = self.startBackgroundTask() + let task = AGSGeodatabaseSyncTask(url: URL) // hold on to task so that it stays retained while it's loading @@ -248,11 +251,9 @@ class JobManagerExample: TableViewController { task.load { [weak self, weak task] error in // make sure we are still around... - guard let self = self else { - return - } - - guard let strongTask = task else { + guard let self = self, let strongTask = task else { + // don't need to end the background task here as that + // would have been done in dealloc return } @@ -262,11 +263,8 @@ class JobManagerExample: TableViewController { } // return if error or no featureServiceInfo - guard error == nil else { - return - } - - guard let fsi = strongTask.featureServiceInfo else { + guard error == nil, let fsi = strongTask.featureServiceInfo else { + self.endBackgroundTask(backgroundTaskIdentifier) return } @@ -299,9 +297,6 @@ class JobManagerExample: TableViewController { // register the job with our JobManager shared instance JobManager.shared.register(job: job) - - // try to keep the app running in the background for this job if possible - let backgroundTaskIdentifier = self.startBackgroundTask() // start the job job.start( @@ -334,6 +329,9 @@ class JobManagerExample: TableViewController { } func takeOffline(map: AGSMap, extent: AGSEnvelope) { + // try to keep the app running in the background for this job if possible + let backgroundTaskIdentifier = self.startBackgroundTask() + let task = AGSOfflineMapTask(onlineMap: map) let uuid = NSUUID() @@ -342,6 +340,8 @@ class JobManagerExample: TableViewController { task.defaultGenerateOfflineMapParameters(withAreaOfInterest: extent) { [weak self] params, error in // make sure we are still around... guard let self = self else { + // don't need to end the background task here as that + // would have been done in dealloc return } @@ -350,9 +350,6 @@ class JobManagerExample: TableViewController { // register the job with our JobManager shared instance JobManager.shared.register(job: job) - - // try to keep the app running in the background for this job if possible - let backgroundTaskIdentifier = self.startBackgroundTask() // start the job job.start( @@ -370,6 +367,7 @@ class JobManagerExample: TableViewController { } else { // if could not get default parameters, then fire completion with the error self.jobCompletionHandler(result: nil, error: error) + self.endBackgroundTask(backgroundTaskIdentifier) } } } From ea3de9793982d5d82d2a1f47b56b66a677e0fd0e Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 14:33:22 -0800 Subject: [PATCH 30/47] pr feedback --- Examples/ArcGISToolkitExamples/JobManagerExample.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 79002600..54e6076d 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -175,6 +175,12 @@ class JobManagerExample: TableViewController { super.viewWillDisappear(animated) } + deinit { + // clear out background tasks that we started for the jobs + backgroundTaskIdentifiers.forEach { UIApplication.shared.endBackgroundTask($0) } + backgroundTaskIdentifiers.removeAll() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if let toolbar = toolbar, toolbar.items == nil { @@ -253,7 +259,7 @@ class JobManagerExample: TableViewController { // make sure we are still around... guard let self = self, let strongTask = task else { // don't need to end the background task here as that - // would have been done in dealloc + // would have been done in deinit return } @@ -341,7 +347,7 @@ class JobManagerExample: TableViewController { // make sure we are still around... guard let self = self else { // don't need to end the background task here as that - // would have been done in dealloc + // would have been done in deinit return } From e9c04e3093d8319741aac3adfb92aa9c47b811ed Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 14:34:22 -0800 Subject: [PATCH 31/47] pr feedback --- .../JobManagerExample.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 54e6076d..1d173e47 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -181,6 +181,20 @@ class JobManagerExample: TableViewController { backgroundTaskIdentifiers.removeAll() } + func startBackgroundTask() -> UIBackgroundTaskIdentifier { + var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + backgroundTaskIdentifiers.insert(backgroundTaskIdentifier) + return backgroundTaskIdentifier + } + + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { + UIApplication.shared.endBackgroundTask(identifier) + backgroundTaskIdentifiers.remove(identifier) + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if let toolbar = toolbar, toolbar.items == nil { @@ -320,20 +334,6 @@ class JobManagerExample: TableViewController { } } - func startBackgroundTask() -> UIBackgroundTaskIdentifier { - var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { - UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) - } - backgroundTaskIdentifiers.insert(backgroundTaskIdentifier) - return backgroundTaskIdentifier - } - - func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { - UIApplication.shared.endBackgroundTask(identifier) - backgroundTaskIdentifiers.remove(identifier) - } - func takeOffline(map: AGSMap, extent: AGSEnvelope) { // try to keep the app running in the background for this job if possible let backgroundTaskIdentifier = self.startBackgroundTask() From 769d18cbc215ca49248f89e9fb7c1c87f2ed80d9 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 14:39:03 -0800 Subject: [PATCH 32/47] pr feedback --- Examples/ArcGISToolkitExamples/Info.plist | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 39f45da8..cfa1dfcf 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -32,9 +32,7 @@ NSPhotoLibraryUsageDescription For collecting attachments in popups UIBackgroundModes - - fetch - + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile From 574ea1a5741de5a31f072ecac1a5ffce6181cef7 Mon Sep 17 00:00:00 2001 From: Ryan Olson Date: Wed, 8 Jan 2020 14:59:32 -0800 Subject: [PATCH 33/47] pr feedback --- .../JobManagerExample.swift | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 1d173e47..aed80b8b 100644 --- a/Examples/ArcGISToolkitExamples/JobManagerExample.swift +++ b/Examples/ArcGISToolkitExamples/JobManagerExample.swift @@ -22,17 +22,10 @@ import UserNotifications // restart the application, and find out what jobs were running and have the ability to // resume them. // -// The other aspect of this sample is that if you just background the app then it will -// provide a helper method that helps with background fetch. -// -// See the AppDelegate.swift for implementation of the function: -// `func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)` -// We forward that call to the shared JobManager so that it can perform the background fetch. -// class JobTableViewCell: UITableViewCell { var job: AGSJob? - var statusObservation: NSKeyValueObservation? + var observation: NSKeyValueObservation? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) @@ -44,15 +37,15 @@ class JobTableViewCell: UITableViewCell { func configureWithJob(job: AGSJob?) { // invalidate previous observation - statusObservation?.invalidate() - statusObservation = nil + observation?.invalidate() + observation = nil self.job = job self.updateUI() - // observe job status - statusObservation = self.job?.observe(\.status, options: .new) { [weak self] (_, _) in + // observe job + observation = self.job?.progress.observe(\.fractionCompleted) { [weak self] (_, _) in DispatchQueue.main.async { self?.updateUI() } @@ -178,7 +171,6 @@ class JobManagerExample: TableViewController { deinit { // clear out background tasks that we started for the jobs backgroundTaskIdentifiers.forEach { UIApplication.shared.endBackgroundTask($0) } - backgroundTaskIdentifiers.removeAll() } func startBackgroundTask() -> UIBackgroundTaskIdentifier { From f3799682d6f8cd6e03b0681b80c62a534752f58f Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 16 Jan 2020 12:41:58 -0600 Subject: [PATCH 34/47] Add @since 100.8 --- Toolkit/ArcGISToolkit/DataSource.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift index be93d86a..cb58d827 100644 --- a/Toolkit/ArcGISToolkit/DataSource.swift +++ b/Toolkit/ArcGISToolkit/DataSource.swift @@ -20,7 +20,7 @@ import ArcGIS public class DataSource: NSObject { /// Returns a `DataSource` initialized with the given `AGSLayerContent` array.. /// - Parameter layers: The array of `AGSLayerContent`. - /// - Since: 100.7.0 + /// - Since: 100.8.0 public init(layers: [AGSLayerContent]) { super.init() layerContents.append(contentsOf: layers) @@ -28,7 +28,7 @@ public class DataSource: NSObject { /// Returns a `DataSource` initialized with the operational and base map layers of a map or scene in an `AGSGeoView`. /// - Parameter geoView: The `AGSGeoView` containing the map/scene's operational and base map layers. - /// - Since: 100.7.0 + /// - Since: 100.8.0 public init(geoView: AGSGeoView) { super.init() self.geoView = geoView @@ -37,7 +37,7 @@ public class DataSource: NSObject { /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and base map layers to use as data. /// If the `DataSource` was initialized with an array of `AGSLayerContent`, `goeView` will be nil. - /// - Since: 100.7.0 + /// - Since: 100.8.0 public private(set) var geoView: AGSGeoView? { didSet { geoViewDidChange(oldValue) @@ -47,7 +47,7 @@ public class DataSource: NSObject { /// The list of all layers used to generate the TOC/Legend, read-only. It contains both the operational layers of the map/scene and /// the reference and base layers of the basemap. The order of the layer contents is the order in which they are drawn /// in a map or scene: bottom up (the first layer in the array is at the bottom and drawn first; the last layer is at the top and drawn last). - /// - Since: 100.7.0 + /// - Since: 100.8.0 public private(set) var layerContents = [AGSLayerContent]() private func geoViewDidChange(_ previousGeoView: AGSGeoView?) { From 123f87b57f0f4ee5419c8fd00bd04cb1d561272e Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 16 Jan 2020 13:46:30 -0600 Subject: [PATCH 35/47] Cleanup and additional test for boundary conditions. --- Toolkit/ArcGISToolkit/DataSource.swift | 35 +++++++----- .../ArcGISToolkitTests/DataSourceTests.swift | 53 ++++++++++++++++++- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift index cb58d827..8fd8be7f 100644 --- a/Toolkit/ArcGISToolkit/DataSource.swift +++ b/Toolkit/ArcGISToolkit/DataSource.swift @@ -1,5 +1,5 @@ // -// Copyright 2019 Esri. +// Copyright 2020 Esri. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ import UIKit import ArcGIS -/// The data source is used to represent an array of `AGSLayerContent` for use in a variety of implementations. It is iinitialized with either an array of `AGSLayerContent` -/// or an `AGSGeoView` of which the operational and base map layers (`AGSLayerContent`) are identified. +/// The data source is used to represent an array of `AGSLayerContent` for use in a variety of +/// implementations. It is iinitialized with either an array of `AGSLayerContent` +/// or an `AGSGeoView` from whose `AGSMap` or `AGSScene`the operational and +/// base map layers (`AGSLayerContent`) are extracted. public class DataSource: NSObject { /// Returns a `DataSource` initialized with the given `AGSLayerContent` array.. /// - Parameter layers: The array of `AGSLayerContent`. @@ -26,8 +28,10 @@ public class DataSource: NSObject { layerContents.append(contentsOf: layers) } - /// Returns a `DataSource` initialized with the operational and base map layers of a map or scene in an `AGSGeoView`. - /// - Parameter geoView: The `AGSGeoView` containing the map/scene's operational and base map layers. + /// Returns a `DataSource` initialized with the operational and base map layers of a + /// map or scene in an `AGSGeoView`. + /// - Parameter geoView: The `AGSGeoView` containing the map/scene's + /// operational and base map layers. /// - Since: 100.8.0 public init(geoView: AGSGeoView) { super.init() @@ -35,7 +39,8 @@ public class DataSource: NSObject { geoViewDidChange(nil) } - /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and base map layers to use as data. + /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and + /// base map layers to use as data. /// If the `DataSource` was initialized with an array of `AGSLayerContent`, `goeView` will be nil. /// - Since: 100.8.0 public private(set) var geoView: AGSGeoView? { @@ -44,16 +49,19 @@ public class DataSource: NSObject { } } - /// The list of all layers used to generate the TOC/Legend, read-only. It contains both the operational layers of the map/scene and - /// the reference and base layers of the basemap. The order of the layer contents is the order in which they are drawn - /// in a map or scene: bottom up (the first layer in the array is at the bottom and drawn first; the last layer is at the top and drawn last). + /// The list of all layers used to generate the TOC/Legend, read-only. It contains both the + /// operational layers of the map/scene and the reference and base layers of the basemap. + /// The order of the layer contents is the order in which they are drawn + /// in a map or scene: bottom up (the first layer in the array is at the bottom and drawn first; the last + /// layer is at the top and drawn last). /// - Since: 100.8.0 public private(set) var layerContents = [AGSLayerContent]() private func geoViewDidChange(_ previousGeoView: AGSGeoView?) { if let mapView = geoView as? AGSMapView { mapView.map?.load { [weak self] (error) in - guard let self = self, let mapView = self.geoView as? AGSMapView else { return } + guard let self = self, + let mapView = self.geoView as? AGSMapView else { return } if let error = error { print("Error loading map: \(error)") } else { @@ -63,9 +71,10 @@ public class DataSource: NSObject { } } else if let sceneView = geoView as? AGSSceneView { sceneView.scene?.load { [weak self] (error) in - guard let self = self, let sceneView = self.geoView as? AGSSceneView else { return } + guard let self = self, + let sceneView = self.geoView as? AGSSceneView else { return } if let error = error { - print("Error loading map: \(error)") + print("Error loading scene: \(error)") } else { self.layerContents = sceneView.scene?.operationalLayers as? [AGSLayerContent] ?? [] self.appendBasemap(sceneView.scene?.basemap) @@ -74,7 +83,7 @@ public class DataSource: NSObject { } } - func appendBasemap(_ basemap: AGSBasemap?) { + private func appendBasemap(_ basemap: AGSBasemap?) { guard let basemap = basemap else { return } basemap.load { [weak self] (error) in diff --git a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift index 75b9218d..60929cdc 100644 --- a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift +++ b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift @@ -1,5 +1,5 @@ // -// Copyright 2019 Esri. +// Copyright 2020 Esri. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -58,6 +58,57 @@ class DataSourceTests: XCTestCase { XCTAssertEqual(layerCount(scene: scene), dataSource.layerContents.count) } + /// Tests boundary conditions by verifying the `layerContents` property returns the correct number of layerContents + func testBoundaryConditions() { + // + // Test with no basemap. + // + let sceneView = AGSSceneView() + let scene = AGSScene() + sceneView.scene = scene + + let layerContents = generateLayerContents() + scene.operationalLayers.addObjects(from: layerContents) + + // Wait for the scene to load. This allows the observers to be set up. + XCTLoad(scene) + + // Create the dataSource. + var dataSource = DataSource(geoView: sceneView) + XCTAssertEqual(layerCount(scene: scene), dataSource.layerContents.count) + + // + // Test with no operational layers. + // + var mapView = AGSMapView() + var map = AGSMap(basemap: .streets()) + mapView.map = map + + // Wait for the map to load. This allows the observers to be set up. + XCTLoad(map) + + // Create the dataSource. + dataSource = DataSource(geoView: mapView) + XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) + + // + // Test with no layers at all. + // + mapView = AGSMapView() + map = AGSMap(spatialReference: .wgs84()) + mapView.map = map + + // Wait for the map to load. This allows the observers to be set up. + XCTLoad(map) + + // Create the dataSource. + dataSource = DataSource(geoView: mapView) + XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) + XCTAssertEqual(dataSource.layerContents.count, 0) + } + + // MARK: Internal + /// Generates a list of predefined layerContents for testing. func generateLayerContents() -> [AGSLayerContent] { let featureTables: [AGSFeatureTable] = [ From 92662f8f4a1411c1b0199580beaeffe56fd42941 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 16 Jan 2020 15:16:53 -0600 Subject: [PATCH 36/47] Remove unused argument to geoViewDidChange. --- Toolkit/ArcGISToolkit/DataSource.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift index 8fd8be7f..7ec269cf 100644 --- a/Toolkit/ArcGISToolkit/DataSource.swift +++ b/Toolkit/ArcGISToolkit/DataSource.swift @@ -36,7 +36,7 @@ public class DataSource: NSObject { public init(geoView: AGSGeoView) { super.init() self.geoView = geoView - geoViewDidChange(nil) + geoViewDidChange() } /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and @@ -45,7 +45,7 @@ public class DataSource: NSObject { /// - Since: 100.8.0 public private(set) var geoView: AGSGeoView? { didSet { - geoViewDidChange(oldValue) + geoViewDidChange() } } @@ -57,7 +57,7 @@ public class DataSource: NSObject { /// - Since: 100.8.0 public private(set) var layerContents = [AGSLayerContent]() - private func geoViewDidChange(_ previousGeoView: AGSGeoView?) { + private func geoViewDidChange() { if let mapView = geoView as? AGSMapView { mapView.map?.load { [weak self] (error) in guard let self = self, From 3e9fcaaecec77ebf6189526a0f6171039beb25c6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Thu, 16 Jan 2020 15:25:17 -0600 Subject: [PATCH 37/47] Add empty layers test and doc updates. --- Toolkit/ArcGISToolkitTests/DataSourceTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift index 60929cdc..5e6f6af6 100644 --- a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift +++ b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift @@ -61,7 +61,7 @@ class DataSourceTests: XCTestCase { /// Tests boundary conditions by verifying the `layerContents` property returns the correct number of layerContents func testBoundaryConditions() { // - // Test with no basemap. + // Test with no base map. // let sceneView = AGSSceneView() let scene = AGSScene() @@ -92,7 +92,7 @@ class DataSourceTests: XCTestCase { XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) // - // Test with no layers at all. + // Test with no operational or base map layers. // mapView = AGSMapView() map = AGSMap(spatialReference: .wgs84()) @@ -105,6 +105,12 @@ class DataSourceTests: XCTestCase { dataSource = DataSource(geoView: mapView) XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) XCTAssertEqual(dataSource.layerContents.count, 0) + + // + // Test with empty layers array. + // + let emptyDataSource = DataSource(layers: []) + XCTAssertEqual(emptyDataSource.layerContents.count, 0) } // MARK: Internal From e59b45d96b5648627468b4b8f642feadf5bc34f1 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 27 Jan 2020 15:07:58 -0600 Subject: [PATCH 38/47] minor doc fixes --- Toolkit/ArcGISToolkit/DataSource.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift index 7ec269cf..98e64976 100644 --- a/Toolkit/ArcGISToolkit/DataSource.swift +++ b/Toolkit/ArcGISToolkit/DataSource.swift @@ -16,8 +16,8 @@ import UIKit import ArcGIS /// The data source is used to represent an array of `AGSLayerContent` for use in a variety of -/// implementations. It is iinitialized with either an array of `AGSLayerContent` -/// or an `AGSGeoView` from whose `AGSMap` or `AGSScene`the operational and +/// implementations. It is initialized with either an array of `AGSLayerContent` +/// or an `AGSGeoView` from whose `AGSMap` or `AGSScene` the operational and /// base map layers (`AGSLayerContent`) are extracted. public class DataSource: NSObject { /// Returns a `DataSource` initialized with the given `AGSLayerContent` array.. From 4c0961612736fdc4b81c5652a4ab4a56a73d96d8 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Wed, 29 Jan 2020 15:16:09 -0600 Subject: [PATCH 39/47] Update README.md Add `Bookmarks` link to TOC. --- Documentation/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/README.md b/Documentation/README.md index bbd6a561..a54d38f0 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -7,3 +7,4 @@ * [Scalebar](Scalebar) * [TimeSlider](TimeSlider) * [AR](AR) +* [Bookmarks](Bookmarks) From 8cc13d707f4279b5248c66a16ec9d0d8078b7bad Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Mon, 6 Apr 2020 15:44:58 -0500 Subject: [PATCH 40/47] Updated deployment target to 12.0 to match the SDK. --- Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 3ba84b0b..e739bf0b 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -439,7 +439,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -493,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -514,6 +514,7 @@ "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", ); INFOPLIST_FILE = ArcGISToolkitExamples/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -534,6 +535,7 @@ "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic", ); INFOPLIST_FILE = ArcGISToolkitExamples/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; From 52aa333331a1ab259d3e6e7358acfef4ecb4e42d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 11:34:35 -0500 Subject: [PATCH 41/47] Update Minimum Toolkit target to 12.0 --- Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index cd382049..3855e727 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -434,7 +434,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -491,7 +491,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -517,6 +517,7 @@ ); INFOPLIST_FILE = ArcGISToolkit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -541,6 +542,7 @@ ); INFOPLIST_FILE = ArcGISToolkit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; From 996a32eab1715e009d39cb7183ff3e85fcbc9ea3 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 11:35:58 -0500 Subject: [PATCH 42/47] Update version to 100.8 --- Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj | 2 ++ Examples/ArcGISToolkitExamples/Info.plist | 2 +- Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj | 2 ++ Toolkit/ArcGISToolkit/Info.plist | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index e739bf0b..2e0889be 100644 --- a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj +++ b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj @@ -516,6 +516,7 @@ INFOPLIST_FILE = ArcGISToolkitExamples/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -537,6 +538,7 @@ INFOPLIST_FILE = ArcGISToolkitExamples/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index cfa1dfcf..4e298317 100644 --- a/Examples/ArcGISToolkitExamples/Info.plist +++ b/Examples/ArcGISToolkitExamples/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 100.6 + $(MARKETING_VERSION) CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index 3855e727..b487f864 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -519,6 +519,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -544,6 +545,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Toolkit/ArcGISToolkit/Info.plist b/Toolkit/ArcGISToolkit/Info.plist index aa1b0165..ec0cc7b0 100644 --- a/Toolkit/ArcGISToolkit/Info.plist +++ b/Toolkit/ArcGISToolkit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 100.6 + $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass From 517be08f9c3bb90503feb3b66bdc0b5483d9da95 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 12:00:26 -0500 Subject: [PATCH 43/47] Fix iOS 12.0 deprecations. --- Toolkit/ArcGISToolkit/TemplatePickerViewController.swift | 2 +- Toolkit/ArcGISToolkit/UnitsViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift b/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift index 9b7317d8..26e86350 100644 --- a/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift +++ b/Toolkit/ArcGISToolkit/TemplatePickerViewController.swift @@ -98,7 +98,7 @@ public class TemplatePickerViewController: TableViewController { private func makeSearchController() -> UISearchController { let searchController = UISearchController(searchResultsController: nil) - searchController.dimsBackgroundDuringPresentation = false + searchController.obscuresBackgroundDuringPresentation = false searchController.hidesNavigationBarDuringPresentation = false searchController.searchResultsUpdater = self diff --git a/Toolkit/ArcGISToolkit/UnitsViewController.swift b/Toolkit/ArcGISToolkit/UnitsViewController.swift index 9083663e..f7014155 100644 --- a/Toolkit/ArcGISToolkit/UnitsViewController.swift +++ b/Toolkit/ArcGISToolkit/UnitsViewController.swift @@ -111,7 +111,7 @@ public class UnitsViewController: TableViewController { /// - Returns: A configured search controller. private func makeSearchController() -> UISearchController { let searchController = UISearchController(searchResultsController: nil) - searchController.dimsBackgroundDuringPresentation = false + searchController.obscuresBackgroundDuringPresentation = false searchController.hidesNavigationBarDuringPresentation = false searchController.searchResultsUpdater = self From 6d4eca3f887555aa9112dbfe515dbe36034dd256 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 12:51:45 -0500 Subject: [PATCH 44/47] Update minimum versions. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85578222..3af33ceb 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ Toolkit components that will simplify your iOS app development with ArcGIS Runti * [Bookmarks](Documentation/Bookmarks) ## Requirements -* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.7.0 (or higher) +* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.8.0 (or higher) * Xcode 10.2 (or higher) -The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *11.0*, meaning that it can run on devices with *iOS 11.0* or newer. +The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *12.0*, meaning that it can run on devices with *iOS 12.0* or newer. ## Instructions From 1f420e39b678fc2934612cdecb913cec16eca758 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 12:54:41 -0500 Subject: [PATCH 45/47] remove test target override of deployment version. --- Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index b487f864..f42b8323 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -565,7 +565,6 @@ FRAMEWORK_SEARCH_PATHS = "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ArcGISToolkitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -588,7 +587,6 @@ FRAMEWORK_SEARCH_PATHS = "$(USER_LIBRARY_DIR)/SDKs/ArcGIS/iOS/Frameworks/Dynamic"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ArcGISToolkitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkitTests; From 579d4616fe9c4145d896a0df97043d95cacf50c6 Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 13:21:53 -0500 Subject: [PATCH 46/47] Updated min XCode to 11.0 to match 100.8 SDK. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3af33ceb..aafc2e3d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Toolkit components that will simplify your iOS app development with ArcGIS Runti ## Requirements * [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.8.0 (or higher) -* Xcode 10.2 (or higher) +* Xcode 11.0 (or higher) The *ArcGIS Runtime Toolkit for iOS* has a *Target SDK* version of *12.0*, meaning that it can run on devices with *iOS 12.0* or newer. From b9ff9e8f68d40d6c1ca6acbc26dd66ec0fbc282d Mon Sep 17 00:00:00 2001 From: Mark Dostal Date: Tue, 7 Apr 2020 13:53:06 -0500 Subject: [PATCH 47/47] Remove DataSource and associated tests. --- .../ArcGISToolkit.xcodeproj/project.pbxproj | 8 - Toolkit/ArcGISToolkit/DataSource.swift | 105 ------------ .../ArcGISToolkitTests/DataSourceTests.swift | 161 ------------------ 3 files changed, 274 deletions(-) delete mode 100644 Toolkit/ArcGISToolkit/DataSource.swift delete mode 100644 Toolkit/ArcGISToolkitTests/DataSourceTests.swift diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index cd382049..03a31225 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -29,8 +29,6 @@ E46893291FEDAE36008ADA79 /* Compass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46893281FEDAE36008ADA79 /* Compass.swift */; }; E46EF2C1236C8A8600A8C50B /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46EF2BF236C8A8600A8C50B /* BookmarksViewController.swift */; }; E46EF2C7236CA16800A8C50B /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46EF2C5236CA16800A8C50B /* BookmarksTableViewController.swift */; }; - E47DDD2C23870C8E000414D1 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47DDD2B23870C8D000414D1 /* DataSource.swift */; }; - E47DDD2E23870C97000414D1 /* DataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47DDD2D23870C97000414D1 /* DataSourceTests.swift */; }; E48405731E9BE7B700927208 /* LegendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48405721E9BE7B700927208 /* LegendViewController.swift */; }; E484057A1E9C262D00927208 /* Legend.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E48405791E9C262D00927208 /* Legend.storyboard */; }; E4ED3D8E237F19B800D835F6 /* BookmarksViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */; }; @@ -85,8 +83,6 @@ E46893281FEDAE36008ADA79 /* Compass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compass.swift; sourceTree = ""; }; E46EF2BF236C8A8600A8C50B /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = ""; }; E46EF2C5236CA16800A8C50B /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = ""; }; - E47DDD2B23870C8D000414D1 /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; - E47DDD2D23870C97000414D1 /* DataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceTests.swift; sourceTree = ""; }; E48405721E9BE7B700927208 /* LegendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegendViewController.swift; sourceTree = ""; }; E48405791E9C262D00927208 /* Legend.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Legend.storyboard; sourceTree = ""; }; E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewControllerTests.swift; sourceTree = ""; }; @@ -154,7 +150,6 @@ 88B689ED1E96EF0800B67FAB /* Misc */ = { isa = PBXGroup; children = ( - E47DDD2B23870C8D000414D1 /* DataSource.swift */, 88B689F01E96EFD700B67FAB /* Extensions.swift */, 88B689F31E96EFD700B67FAB /* MapViewController.swift */, 88B689FA1E96EFD700B67FAB /* TableViewController.swift */, @@ -195,7 +190,6 @@ E4685B04237CA3C400F168CF /* ArcGISToolkitTests */ = { isa = PBXGroup; children = ( - E47DDD2D23870C97000414D1 /* DataSourceTests.swift */, E4685B05237CA3C400F168CF /* ArcGISToolkitTests.swift */, E4685B07237CA3C400F168CF /* Info.plist */, E4ED3D8D237F19B800D835F6 /* BookmarksViewControllerTests.swift */, @@ -353,7 +347,6 @@ 2140781C209B628200FBFDCC /* TimeSlider.swift in Sources */, 88DBC2A31FE83DB800255921 /* CancelGroup.swift in Sources */, 88B68A071E96EFD700B67FAB /* TableViewController.swift in Sources */, - E47DDD2C23870C8E000414D1 /* DataSource.swift in Sources */, 88B689FD1E96EFD700B67FAB /* Extensions.swift in Sources */, 88B68A741E9D7F6300B67FAB /* Coalescer.swift in Sources */, 88B68A001E96EFD700B67FAB /* MapViewController.swift in Sources */, @@ -366,7 +359,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E47DDD2E23870C97000414D1 /* DataSourceTests.swift in Sources */, E4ED3D8E237F19B800D835F6 /* BookmarksViewControllerTests.swift in Sources */, E4685B06237CA3C400F168CF /* ArcGISToolkitTests.swift in Sources */, ); diff --git a/Toolkit/ArcGISToolkit/DataSource.swift b/Toolkit/ArcGISToolkit/DataSource.swift deleted file mode 100644 index 98e64976..00000000 --- a/Toolkit/ArcGISToolkit/DataSource.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright 2020 Esri. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import UIKit -import ArcGIS - -/// The data source is used to represent an array of `AGSLayerContent` for use in a variety of -/// implementations. It is initialized with either an array of `AGSLayerContent` -/// or an `AGSGeoView` from whose `AGSMap` or `AGSScene` the operational and -/// base map layers (`AGSLayerContent`) are extracted. -public class DataSource: NSObject { - /// Returns a `DataSource` initialized with the given `AGSLayerContent` array.. - /// - Parameter layers: The array of `AGSLayerContent`. - /// - Since: 100.8.0 - public init(layers: [AGSLayerContent]) { - super.init() - layerContents.append(contentsOf: layers) - } - - /// Returns a `DataSource` initialized with the operational and base map layers of a - /// map or scene in an `AGSGeoView`. - /// - Parameter geoView: The `AGSGeoView` containing the map/scene's - /// operational and base map layers. - /// - Since: 100.8.0 - public init(geoView: AGSGeoView) { - super.init() - self.geoView = geoView - geoViewDidChange() - } - - /// The `AGSGeoView` containing either an `AGSMap` or `AGSScene` with the operational and - /// base map layers to use as data. - /// If the `DataSource` was initialized with an array of `AGSLayerContent`, `goeView` will be nil. - /// - Since: 100.8.0 - public private(set) var geoView: AGSGeoView? { - didSet { - geoViewDidChange() - } - } - - /// The list of all layers used to generate the TOC/Legend, read-only. It contains both the - /// operational layers of the map/scene and the reference and base layers of the basemap. - /// The order of the layer contents is the order in which they are drawn - /// in a map or scene: bottom up (the first layer in the array is at the bottom and drawn first; the last - /// layer is at the top and drawn last). - /// - Since: 100.8.0 - public private(set) var layerContents = [AGSLayerContent]() - - private func geoViewDidChange() { - if let mapView = geoView as? AGSMapView { - mapView.map?.load { [weak self] (error) in - guard let self = self, - let mapView = self.geoView as? AGSMapView else { return } - if let error = error { - print("Error loading map: \(error)") - } else { - self.layerContents = mapView.map?.operationalLayers as? [AGSLayerContent] ?? [] - self.appendBasemap(mapView.map?.basemap) - } - } - } else if let sceneView = geoView as? AGSSceneView { - sceneView.scene?.load { [weak self] (error) in - guard let self = self, - let sceneView = self.geoView as? AGSSceneView else { return } - if let error = error { - print("Error loading scene: \(error)") - } else { - self.layerContents = sceneView.scene?.operationalLayers as? [AGSLayerContent] ?? [] - self.appendBasemap(sceneView.scene?.basemap) - } - } - } - } - - private func appendBasemap(_ basemap: AGSBasemap?) { - guard let basemap = basemap else { return } - - basemap.load { [weak self] (error) in - if let error = error { - print("Error loading base map: \(error)") - } else { - // Append any reference layers to the `layerContents` array. - if let referenceLayers = basemap.referenceLayers as? [AGSLayerContent] { - self?.layerContents.append(contentsOf: referenceLayers) - } - - // Insert any base layers at the beginning of the `layerContents` array. - if let baseLayers = basemap.baseLayers as? [AGSLayerContent] { - self?.layerContents.insert(contentsOf: baseLayers, at: 0) - } - } - } - } -} diff --git a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift b/Toolkit/ArcGISToolkitTests/DataSourceTests.swift deleted file mode 100644 index 5e6f6af6..00000000 --- a/Toolkit/ArcGISToolkitTests/DataSourceTests.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// Copyright 2020 Esri. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import XCTest -import ArcGISToolkit -import ArcGIS - -class DataSourceTests: XCTestCase { - /// Tests the creation of a `DataSource` using a list of `AGSLayerContent`. - func testDataSourceLayers() { - let layers = generateLayerContents() - let dataSource = DataSource(layers: layers) - XCTAssertEqual(layers.count, dataSource.layerContents.count) - } - - /// Tests the creation of a `DataSource` using an `AGSMapView` by verifying the `layerContents` property returns the correct number of `AGSLayerContent`. - func testDataSourceMapView() { - let mapView = AGSMapView() - let map = AGSMap(basemap: .streets()) - mapView.map = map - - let layerContents = generateLayerContents() - map.operationalLayers.addObjects(from: layerContents) - - // Wait for the map to load. This allows the observers to be set up. - XCTLoad(map) - - // Create the dataSource. - let dataSource = DataSource(geoView: mapView) - XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) - } - - /// Tests the creation of the `dataSource` using an `AGSSceneView` by verifying the `layerContents` property returns the correct number of layerContents - func testlayerContentsSceneView() { - let sceneView = AGSSceneView() - let scene = AGSScene(basemap: .streets()) - sceneView.scene = scene - - let layerContents = generateLayerContents() - scene.operationalLayers.addObjects(from: layerContents) - - // Wait for the scene to load. This allows the observers to be set up. - XCTLoad(scene) - - // Create the dataSource. - let dataSource = DataSource(geoView: sceneView) - XCTAssertEqual(layerCount(scene: scene), dataSource.layerContents.count) - } - - /// Tests boundary conditions by verifying the `layerContents` property returns the correct number of layerContents - func testBoundaryConditions() { - // - // Test with no base map. - // - let sceneView = AGSSceneView() - let scene = AGSScene() - sceneView.scene = scene - - let layerContents = generateLayerContents() - scene.operationalLayers.addObjects(from: layerContents) - - // Wait for the scene to load. This allows the observers to be set up. - XCTLoad(scene) - - // Create the dataSource. - var dataSource = DataSource(geoView: sceneView) - XCTAssertEqual(layerCount(scene: scene), dataSource.layerContents.count) - - // - // Test with no operational layers. - // - var mapView = AGSMapView() - var map = AGSMap(basemap: .streets()) - mapView.map = map - - // Wait for the map to load. This allows the observers to be set up. - XCTLoad(map) - - // Create the dataSource. - dataSource = DataSource(geoView: mapView) - XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) - - // - // Test with no operational or base map layers. - // - mapView = AGSMapView() - map = AGSMap(spatialReference: .wgs84()) - mapView.map = map - - // Wait for the map to load. This allows the observers to be set up. - XCTLoad(map) - - // Create the dataSource. - dataSource = DataSource(geoView: mapView) - XCTAssertEqual(layerCount(map: map), dataSource.layerContents.count) - XCTAssertEqual(dataSource.layerContents.count, 0) - - // - // Test with empty layers array. - // - let emptyDataSource = DataSource(layers: []) - XCTAssertEqual(emptyDataSource.layerContents.count, 0) - } - - // MARK: Internal - - /// Generates a list of predefined layerContents for testing. - func generateLayerContents() -> [AGSLayerContent] { - let featureTables: [AGSFeatureTable] = [ - AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/0")!), - AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/8")!), - AGSServiceFeatureTable(url: URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Geology/FeatureServer/9")!) - ] - - var layers = [AGSLayerContent]() - featureTables.forEach { (featureTable) in - layers.append(AGSFeatureLayer(featureTable: featureTable)) - } - - return layers - } - - /// Counts the number of layers in a map's operational layers and base map. - /// - Parameter map: The map to count the layers in. - func layerCount(map: AGSMap) -> Int { - return layerCount(operationalLayers: map.operationalLayers as! [AGSLayer], basemap: map.basemap) - } - - /// Counts the number of layers in a map's operational layers and base map. - /// - Parameter scene: The scene to count the layers in. - func layerCount(scene: AGSScene) -> Int { - return layerCount(operationalLayers: scene.operationalLayers as! [AGSLayer], basemap: scene.basemap) - } - - /// Counts the total number of layers in an operationalLayers array and basemap. - /// - Parameter operationalLayers: The operational layers to count. - /// - Parameter basemap: The base map containing the base and reference layers to count - func layerCount(operationalLayers: [AGSLayer], basemap: AGSBasemap?) -> Int { - var count = operationalLayers.count - if let refLayers = basemap?.referenceLayers { - count += refLayers.count - } - - if let baseLayers = basemap?.baseLayers { - count += baseLayers.count - } - - return count - } -}