Add, delete, and download attachments for features from a service.
Use case
Attachments provide a flexible way to manage additional information that is related to your features. Attachments allow you to add files to individual features, including: PDFs, text documents, or any other type of file. For example, if you have a feature representing a building, you could use attachments to add multiple photographs of the building taken from several angles, along with PDF files containing the building's deed and tax information.
How to use the sample
Tap a feature on the map to open a callout displaying the number of attachments. Tap on the info button to view/edit the attachments. Select an entry from the list to download and view the attachment in the gallery. Tap on the action button "+" to add an attachment or swipe left on an attachment to delete.
How it works
- Create an
AGSServiceFeatureTable
from a URL. - Create an
AGSFeatureLayer
object from the service feature table. - Find the layer of the selected feature using
AGSGeoView.identifyLayer(_:screenPoint:tolerance:returnPopupsOnly:maximumResults:completion:)
, which provides anAGSIdentifyLayerResult
used to get the selected feature. - To fetch the feature's attachments, use
AGSArcGISFeature.fetchAttachments(completion:)
. - To add an attachment to the selected
AGSArcGISFeature
, create an attachment and useAGSArcGISFeature.addAttachment(withName:contentType:data:completion:)
. - To delete an attachment from the selected
AGSArcGISFeature
, use theAGSArcGISFeature.delete(_:completion:)
. - After a change, apply the changes to the server using
AGSServiceFeatureTable.applyEdits(completion:)
.
Relevant API
- AGSArcGISFeature.delete(_:completion:)
- AGSArcGISFeature.fetchAttachments(completion:)
- AGSFeatureLayer
- AGSServiceFeatureTable
- AGSServiceFeatureTable.applyEdits(completion:)
Additional information
Attachments can only be added to and accessed on a service feature table when its hasAttachments
property is true.
Tags
edit and manage data, image, JPEG, PDF, picture, PNG, TXT
Sample Code
// Copyright 2016 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 AttachmentsTableViewController: UITableViewController {
weak var feature: AGSArcGISFeature? {
didSet {
loadAttachments()
}
}
private var attachments: [AGSAttachment] = []
private func loadAttachments() {
// show progress hud
UIApplication.shared.showProgressHUD(message: "Loading attachments")
feature?.fetchAttachments { [weak self] (attachments: [AGSAttachment]?, error: Error?) in
// dismiss progress hud
UIApplication.shared.hideProgressHUD()
guard let self = self else {
return
}
if let error = error {
// show the error
self.presentAlert(error: error)
} else if let attachments = attachments {
self.attachments = attachments
self.tableView?.reloadData()
}
}
}
private func deleteAttachment(_ attachment: AGSAttachment) {
feature?.delete(attachment) { [weak self] (error: Error?) in
guard let self = self else {
return
}
if let error = error {
print(error)
} else if let attachmentIndex = self.attachments.firstIndex(of: attachment) {
let indexPathToRemove = IndexPath(row: attachmentIndex, section: 0)
// update the model
self.attachments.remove(at: attachmentIndex)
// update the table
self.tableView.deleteRows(at: [indexPathToRemove], with: .automatic)
}
}
}
private func downloadImage(for attachment: AGSAttachment) {
UIApplication.shared.showProgressHUD(message: "Downloading attachment")
attachment.fetchData { [weak self] (data: Data?, error: Error?) in
UIApplication.shared.hideProgressHUD()
guard let self = self else {
return
}
if let error = error {
print(error)
} else if let data = data,
let index = self.attachments.firstIndex(of: attachment),
let cell = self.tableView.cellForRow(at: IndexPath(row: index, section: 0)) {
cell.imageView?.image = UIImage(data: data)
}
}
}
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return attachments.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AttachmentCell", for: indexPath)
let attachment = attachments[indexPath.row]
cell.textLabel?.text = attachment.name
cell.imageView?.contentMode = .scaleAspectFit
if attachment.hasFetchedData {
attachment.fetchData { (data, _) in
if let data = data {
cell.imageView?.image = UIImage(data: data)
}
}
} else {
cell.imageView?.image = UIImage(named: "CloudDownload", in: AGSBundle(), compatibleWith: nil)
}
return cell
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return !attachments[indexPath.row].hasFetchedData
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
downloadImage(for: attachments[indexPath.row])
tableView.cellForRow(at: indexPath)?.setSelected(false, animated: true)
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let attachment = attachments[indexPath.row]
deleteAttachment(attachment)
}
}
// MARK: - Actions
@IBAction func doneAction() {
if let table = feature?.featureTable as? AGSServiceFeatureTable {
// show progress hud
UIApplication.shared.showProgressHUD(message: "Applying edits")
table.applyEdits { [weak self] (_, error) in
// dismiss progress hud
UIApplication.shared.hideProgressHUD()
guard let self = self else {
return
}
if let error = error {
self.presentAlert(error: error)
}
self.dismiss(animated: true)
}
} else {
dismiss(animated: true)
}
}
@IBAction func addAction() {
guard let pngData = UIImage(named: "LocationDisplayOffIcon")?.pngData() else {
return
}
// show progress hud
UIApplication.shared.showProgressHUD(message: "Adding attachment")
feature?.addAttachment(withName: "Attachment.png", contentType: "png", data: pngData) { [weak self] (attachment: AGSAttachment?, error: Error?) in
UIApplication.shared.hideProgressHUD()
guard let self = self else {
return
}
if let error = error {
self.presentAlert(error: error)
} else if let attachment = attachment {
// new attachments are added to the end
let indexPath = IndexPath(row: self.attachments.count, section: 0)
// update the model
self.attachments.append(attachment)
// update the table
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
}
}
}