Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS Runtime SDK for iOS

Control annotation sublayer visibility

Sample Viewer View Sample on GitHub

Use annotation sublayers to gain finer control of annotation layer subtypes.

Screenshot of Control Annotation Sublayer Visibility sample

Use case

Annotation, which differs from labels by having a fixed place and size, is typically only relevant at particular scales. Annotation sublayers allow for finer control of annotation by allowing properties (like visibility in the map and legend) to be set and others to be read (like name) on subtypes of an annotation layer.

An annotation dataset which marks valves as "Opened" or "Closed", might be set to display the "Closed" valves over a broader range of scales than the "Opened" valves, if the "Closed" data is considered more relevant by the map's author. Regardless, the user can be given a manual option to set visibility of annotation sublayers on and off, if required.

How to use the sample

Open the sample and take note of the visibility of the annotation. Zoom in and out to see the annotation turn on and off based on scale ranges set on the data.

Tap the "Sublayers" button and use the switches to manually set "Open" and "Closed" annotation sublayers visibility to on or off.

How it works

  1. Load an AGSMobileMapPackage that contains an AGSAnnotationLayer.
  2. Get the sublayers from the map package's annotation layers with subLayerContents property.
  3. Toggle the isVisible property to set visibility of each sublayer manually.
  4. To determine if a sublayer is visible at the current scale of the map view, use the AGSLayerContent.isVisible(atScale:) method, by passing in the map's current scale.

Relevant API

  • AGSAnnotationLayer
  • AGSAnnotationSublayer
  • AGSLayerContent

Offline data

This sample uses the Gas Device Anno Mobile Map Package. It is downloaded from ArcGIS Online automatically.

About the data

The scale ranges were set by the map's author using ArcGIS Pro:

  • The "Open" annotation sublayer has its maximum scale set to 1:500 and its minimum scale set to 1:2000.
  • The "Closed" annotation sublayer has no minimum or maximum scales set, so will be drawn at all scales.

Tags

annotation, scale, text, utilities, visualization

Sample Code

//
// 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

protocol ControlAnnotationSublayerVisibilitySublayerCellDelegate: AnyObject {
    func sublayerCellDidToggleSwitch(_ sublayerCell: ControlAnnotationSublayerVisibilitySublayerCell)
}

/// A `UITableViewCell` subclass that displays a label on the left and a switch
/// on the right.
class ControlAnnotationSublayerVisibilitySublayerCell: UITableViewCell {
    /// The switch displayed as the cell's accessory views. Changes to the
    /// switch's value are reported through the delegate.
    let `switch` = UISwitch()
    weak var delegate: ControlAnnotationSublayerVisibilitySublayerCellDelegate?
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        `switch`.addTarget(self, action: #selector(switchValueChanged), for: .valueChanged)
        accessoryView = `switch`
    }
    
    @objc
    private func switchValueChanged() {
        delegate?.sublayerCellDidToggleSwitch(self)
    }
}
//
// 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

class ControlAnnotationSublayerVisibilitySublayersViewController: UITableViewController {
    var annotationSublayers = [AGSAnnotationSublayer]() {
        didSet {
            guard isViewLoaded else { return }
            tableView.reloadData()
        }
    }
    var mapScale = Double.nan {
        didSet {
            mapScaleDidChange(oldValue)
        }
    }
    
    /// The formatter used to generate strings from scale values.
    private let scaleFormatter: NumberFormatter = {
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        numberFormatter.maximumFractionDigits = 0
        return numberFormatter
    }()
    /// The observation of the table view's content size.
    private var tableViewContentSizeObservation: NSKeyValueObservation?
    
    func title(for annotationSublayer: AGSAnnotationSublayer) -> String {
        let maxScale = annotationSublayer.maxScale
        let minScale = annotationSublayer.minScale
        var title = annotationSublayer.name
        if !(maxScale.isNaN || minScale.isNaN) {
            let maxScaleString = scaleFormatter.string(from: maxScale as NSNumber)!
            let minScaleString = scaleFormatter.string(from: minScale as NSNumber)!
            title.append(String(format: " (1:%@ - 1:%@)", maxScaleString, minScaleString))
        }
        return title
    }
    
    func mapScaleDidChange(_ previousMapScale: Double) {
        var indexPaths = [IndexPath]()
        for row in annotationSublayers.indices {
            let annotationSublayer = annotationSublayers[row]
            let wasVisible = annotationSublayer.isVisible(atScale: previousMapScale)
            let isVisible = annotationSublayer.isVisible(atScale: mapScale)
            if isVisible != wasVisible {
                let indexPath = IndexPath(row: row, section: 0)
                indexPaths.append(indexPath)
            }
        }
        if !indexPaths.isEmpty {
            tableView.reloadRows(at: indexPaths, with: .automatic)
        }
    }
    
    // MARK: UIViewController
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        tableViewContentSizeObservation = tableView.observe(\.contentSize) { [unowned self] (tableView, _) in
            self.preferredContentSize.height = tableView.contentSize.height
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        tableViewContentSizeObservation = nil
    }
}

extension ControlAnnotationSublayerVisibilitySublayersViewController: ControlAnnotationSublayerVisibilitySublayerCellDelegate {
    func sublayerCellDidToggleSwitch(_ sublayerCell: ControlAnnotationSublayerVisibilitySublayerCell) {
        guard let indexPath = tableView.indexPath(for: sublayerCell) else {
            return
        }
        
        annotationSublayers[indexPath.row].isVisible = sublayerCell.switch.isOn
    }
}

extension ControlAnnotationSublayerVisibilitySublayersViewController /* UITableViewDataSource */ {
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return annotationSublayers.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let annotationSublayer = annotationSublayers[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "SublayerCell", for: indexPath) as! ControlAnnotationSublayerVisibilitySublayerCell
        cell.textLabel?.text = title(for: annotationSublayer)
        cell.textLabel?.isEnabled = annotationSublayer.isVisible(atScale: mapScale)
        cell.switch.isOn = annotationSublayer.isVisible
        cell.delegate = self
        return cell
    }
}
//
// 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

/// A view controller that manages the interface of the Control Annotation
/// Sublayer Visibility sample.
class ControlAnnotationSublayerVisibilityViewController: UIViewController {
    /// The map view managed by the view controller.
    @IBOutlet var mapView: AGSMapView! {
        didSet {
            loadMobileMapPackage()
        }
    }
    /// The label that display's the map view's map scale.
    @IBOutlet weak var currentMapScaleLabel: UILabel!
    /// The bar button item that presents the sublayers view controller.
    @IBOutlet weak var sublayersButtonItem: UIBarButtonItem!
    
    /// The mobile map package used by this sample.
    let mobileMapPackage = AGSMobileMapPackage(fileURL: Bundle.main.url(forResource: "GasDeviceAnno", withExtension: "mmpk")!)
    /// The sublayers of the annotation layer. Will be empty until the
    /// annotation layer has finished loading.
    var annotationSublayers = [AGSAnnotationSublayer]()
    /// The observation of the map view's map scale.
    var mapScaleObservation: NSKeyValueObservation?
    
    /// The formatter used to generate strings from scale values.
    private let scaleFormatter: NumberFormatter = {
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        numberFormatter.maximumFractionDigits = 0
        return numberFormatter
    }()
    
    /// Initiates loading of the mobile map package.
    func loadMobileMapPackage() {
        mobileMapPackage.load { [weak self] (result: Result<Void, Error>) in
            self?.mobileMapPackageDidLoad(with: result)
        }
    }
    
    /// Called in response to the mobile map package load operation completing.
    ///
    /// - Parameter result: The result of the load operation.
    func mobileMapPackageDidLoad(with result: Result<Void, Error>) {
        switch result {
        case .success:
            let map = mobileMapPackage.maps.first
            mapView.map = map
            if let annotationLayer = map?.operationalLayers.first(where: { $0 is AGSAnnotationLayer }) as? AGSAnnotationLayer {
                annotationLayer.load { [weak self, unowned annotationLayer] (result: Result<Void, Error>) in
                    self?.annotationLayer(annotationLayer, didLoadWith: result)
                }
            }
            sublayersButtonItem.isEnabled = true
        case .failure(let error):
            presentAlert(error: error)
        }
    }
    
    /// Called in response to the annotation layer load operation completing.
    ///
    /// - Parameters:
    ///   - annotationLayer: The annotation layer that finished loading.
    ///   - result: The result of the load operation.
    func annotationLayer(_ annotationLayer: AGSAnnotationLayer, didLoadWith result: Result<Void, Error>) {
        switch result {
        case .success:
            annotationSublayers.append(contentsOf: annotationLayer.subLayerContents.compactMap { $0 as? AGSAnnotationSublayer })
            sublayersButtonItem.isEnabled = true
        case .failure(let error):
            presentAlert(error: error)
        }
    }
    
    /// Called in response to the map view's map scale changing.
    func mapScaleDidChange() {
        // Update the text of the Current Map Scale label.
        let mapScale = mapView.mapScale
        currentMapScaleLabel.text = String(format: "1:%@", scaleFormatter.string(from: mapScale as NSNumber)!)
        // Inform the sublayers view controller of the new map scale.
        let sublayersViewController = self.presentedViewController as? ControlAnnotationSublayerVisibilitySublayersViewController
        sublayersViewController?.mapScale = mapScale
    }
    
    // MARK: UIViewController
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        (navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = [
            "ControlAnnotationSublayerVisibilityViewController",
            "ControlAnnotationSublayerVisibilitySublayersViewController",
            "ControlAnnotationSublayerVisibilitySublayerCell"
        ]
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        mapScaleObservation = mapView.observe(\.mapScale, options: .initial) { [weak self] (_, _) in
            DispatchQueue.main.async { self?.mapScaleDidChange() }
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        mapScaleObservation = nil
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let sublayersViewController = segue.destination as? ControlAnnotationSublayerVisibilitySublayersViewController {
            sublayersViewController.annotationSublayers = annotationSublayers
            sublayersViewController.mapScale = mapView.mapScale
            if let popoverPresentationController = sublayersViewController.popoverPresentationController {
                popoverPresentationController.delegate = self
                popoverPresentationController.passthroughViews = [mapView]
            }
        }
    }
}

extension ControlAnnotationSublayerVisibilityViewController: UIPopoverPresentationControllerDelegate {
    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
        return .none
    }
}

private extension AGSLoadable {
    func load(completion: @escaping (Result<Void, Error>) -> Void) {
        load { (error: Error?) in
            let result: Result<Void, Error>
            if let error = error {
                result = .failure(error)
            } else {
                result = .success(())
            }
            completion(result)
        }
    }
}