diff --git a/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj b/Examples/ArcGISToolkitExamples.xcodeproj/project.pbxproj index 3ba84b0b..2e0889be 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,7 +514,9 @@ "$(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"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -534,7 +536,9 @@ "$(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"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/Examples/ArcGISToolkitExamples/AppDelegate.swift b/Examples/ArcGISToolkitExamples/AppDelegate.swift index 9dce979a..baf37d18 100644 --- a/Examples/ArcGISToolkitExamples/AppDelegate.swift +++ b/Examples/ArcGISToolkitExamples/AppDelegate.swift @@ -44,10 +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) { - JobManager.shared.application(application: application, performFetchWithCompletionHandler: completionHandler) - } } diff --git a/Examples/ArcGISToolkitExamples/Info.plist b/Examples/ArcGISToolkitExamples/Info.plist index 39f45da8..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 @@ -32,9 +32,7 @@ NSPhotoLibraryUsageDescription For collecting attachments in popups UIBackgroundModes - - fetch - + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Examples/ArcGISToolkitExamples/JobManagerExample.swift b/Examples/ArcGISToolkitExamples/JobManagerExample.swift index 9516bbe9..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() } @@ -108,6 +101,8 @@ class JobManagerExample: TableViewController { return JobManager.shared.jobs } + var backgroundTaskIdentifiers = Set() + var toolbar: UIToolbar? override func viewDidLoad() { @@ -129,19 +124,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,9 +136,69 @@ class JobManagerExample: TableViewController { tableView.register(JobTableViewCell.self, forCellReuseIdentifier: "JobCell") } - @objc - func resumeAllPausedJobs() { - JobManager.shared.resumeAllPausedJobs(statusHandler: self.jobStatusHandler, completion: self.jobCompletionHandler) + 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) + }, + completion: { [weak self] in + self?.jobCompletionHandler(result: $0, error: $1) + } + ) + + super.viewDidAppear(animated) + } + + 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 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() + + // clear out background tasks that we started for the jobs + backgroundTaskIdentifiers.forEach { UIApplication.shared.endBackgroundTask($0) } + backgroundTaskIdentifiers.removeAll() + + super.viewWillDisappear(animated) + } + + deinit { + // clear out background tasks that we started for the jobs + backgroundTaskIdentifiers.forEach { UIApplication.shared.endBackgroundTask($0) } + } + + 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 { + // button to kick off a new job + let kickOffJobItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(kickOffJob)) + + // button to clear the finished jobs + let clearFinishedJobsItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(clearFinishedJobs)) + + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.items = [kickOffJobItem, flexibleSpace, clearFinishedJobsItem] + } } @objc @@ -211,6 +253,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 @@ -218,11 +263,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 deinit return } @@ -232,11 +275,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 } @@ -271,7 +311,15 @@ 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) + self?.endBackgroundTask(backgroundTaskIdentifier) + } + ) // refresh the tableview self.tableView.reloadData() @@ -279,6 +327,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() @@ -287,6 +338,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 deinit return } @@ -297,13 +350,22 @@ 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) + self?.endBackgroundTask(backgroundTaskIdentifier) + } + ) // refresh the tableview self.tableView.reloadData() } else { // if could not get default parameters, then fire completion with the error self.jobCompletionHandler(result: nil, error: error) + self.endBackgroundTask(backgroundTaskIdentifier) } } } diff --git a/README.md b/README.md index 52d49933..5a010164 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,10 @@ To use Toolkit in your project: * **[TimeSlider](Documentation/TimeSlider)** - Allows interactively defining a temporal range (i.e. time extent) and animating time moving forward or backward. Can be used to manipulate the time extent in a MapView or SceneView. ## Requirements +* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.8.0 (or higher) +* Xcode 11.0 (or higher) -* [ArcGIS Runtime SDK for iOS](https://developers.arcgis.com/en/ios/) 100.7.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 diff --git a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj index 03a31225..0c664e90 100644 --- a/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj +++ b/Toolkit/ArcGISToolkit.xcodeproj/project.pbxproj @@ -426,7 +426,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; @@ -483,7 +483,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"; @@ -509,7 +509,9 @@ ); 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"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -533,7 +535,9 @@ ); 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"; + MARKETING_VERSION = 100.8; PRODUCT_BUNDLE_IDENTIFIER = com.esri.ArcGISToolkit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -553,7 +557,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; @@ -576,7 +579,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; 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 diff --git a/Toolkit/ArcGISToolkit/JobManager.swift b/Toolkit/ArcGISToolkit/JobManager.swift index c4384d12..9f073926 100644 --- a/Toolkit/ArcGISToolkit/JobManager.swift +++ b/Toolkit/ArcGISToolkit/JobManager.swift @@ -62,14 +62,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 +112,7 @@ public class JobManager: NSObject { jobStatusObservations.removeValue(forKey: job.serverJobID) } } - + /// Register an `AGSJob` with the `JobManager`. /// /// - Parameter job: The AGSJob to register. @@ -123,7 +123,7 @@ public class JobManager: NSObject { keyedJobs[jobUniqueID] = job return jobUniqueID } - + /// Unregister an `AGSJob` from the `JobManager`. /// /// - Parameter job: The job to unregister. @@ -198,6 +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 '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) @@ -228,6 +229,14 @@ public class JobManager: NSObject { } } + /// Pauses any currently running job. + public func pauseAllJobs() { + keyedJobs.values.forEach { + guard $0.status == .started else { return } + $0.progress.pause() + } + } + /// Saves all managed `AGSJob`s to User Defaults. /// /// This happens automatically when the `AGSJob`s are registered/unregistered. diff --git a/Toolkit/ArcGISToolkit/MeasureToolbar.swift b/Toolkit/ArcGISToolkit/MeasureToolbar.swift index baae9de5..3b07d4f1 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,16 +221,9 @@ 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 { - 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, @@ -266,47 +260,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 +321,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 +346,7 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { } else if segControl.selectedSegmentIndex == 2 { startFeatureMode() } + setNeedsLayout() } private func startLineMode() { @@ -405,12 +356,15 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .length selectionOverlay?.isVisible = false - self.items = sketchModeButtons mapView?.sketchEditor = lineSketchEditor if !lineSketchEditor.isStarted { lineSketchEditor.start(with: AGSSketchCreationMode.polyline) } + + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } private func startAreaMode() { @@ -420,12 +374,15 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .area selectionOverlay?.isVisible = false - self.items = sketchModeButtons mapView?.sketchEditor = areaSketchEditor if !areaSketchEditor.isStarted { areaSketchEditor.start(with: AGSSketchCreationMode.polygon) } + + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } private func startFeatureMode() { @@ -435,8 +392,11 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mode = .feature selectionOverlay?.isVisible = true - self.items = selectModeButtons mapView?.sketchEditor = nil + + // updateMeasurement() requires mode property and sketch editor + // properties to be current, so we do this last when changing modes + updateMeasurement() } @objc @@ -454,27 +414,49 @@ public class MeasureToolbar: UIToolbar, AGSGeoViewTouchDelegate { mapView?.sketchEditor?.clearGeometry() } + private lazy var linearUnits: [AGSLinearUnit] = { + let linearUnitIDs: [AGSLinearUnitID] = [.centimeters, .feet, .inches, .kilometers, .meters, .miles, .millimeters, .nauticalMiles, .yards] + return linearUnitIDs + .compactMap(AGSLinearUnit.init) + .sorted { $0.pluralDisplayName < $1.pluralDisplayName } + }() + + private lazy var areaUnits: [AGSAreaUnit] = { + let areaUnitIDs: [AGSAreaUnitID] = [.acres, .hectares, .squareCentimeters, .squareDecimeters, .squareFeet, .squareKilometers, .squareMeters, .squareMillimeters, .squareMiles, .squareYards] + return areaUnitIDs + .compactMap(AGSAreaUnit.init) + .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() } 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