Skip to content

Commit

Permalink
Merge pull request #369 from Esri/v.next
Browse files Browse the repository at this point in the history
[Release] 200.4.0
  • Loading branch information
yo1995 committed Apr 11, 2024
2 parents 05b3a37 + 8597ea3 commit 62fea7a
Show file tree
Hide file tree
Showing 235 changed files with 8,218 additions and 877 deletions.
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
# ArcGIS Maps SDK for Swift Samples

This repository contains Swift sample code demonstrating the capabilities of [ArcGIS Maps SDK for Swift](https://developers.arcgis.com/swift/) and how to use them in your own app. The project can be opened in Xcode and run on a simulator or a device.
This repository contains Swift sample code demonstrating the capabilities of the [ArcGIS Maps SDK for Swift](https://developers.arcgis.com/swift/) and how to use those capabilities in your own app. The project can be opened in Xcode and run on a simulator or a device.

## Features

* Maps - Open, create, interact with and save maps
* Scenes - Visualize 3D environments and symbols
* Layers - Display vector and raster data in maps and scenes
* Augmented Reality - View data overlaid on the real world through your device's camera
* Visualization - Show graphics, popups, callouts, sketches, and style maps with symbols and renderers
* Edit and Manage Data - Add, delete, and edit features and attachments, and taking data offline
* Search and Query - Find addresses, places, and points of interest
* Routing and Logistics - Calculate routes between locations and around barriers
* Analysis - Perform spatial analysis via geoprocessing tasks and services
* Cloud and Portal - Search for web maps and securely connect to your portal
* Utility Networks - Work with utility networks, performing traces and exploring network elements

## Requirements

* [ArcGIS Maps SDK for Swift](https://developers.arcgis.com/swift/) 200.3 (or newer)
* [ArcGIS Maps SDK for Swift Toolkit](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit) 200.3 (or newer)
* [ArcGIS Maps SDK for Swift](https://developers.arcgis.com/swift/) 200.4 (or newer)
* [ArcGIS Maps SDK for Swift Toolkit](https://github.com/Esri/arcgis-maps-sdk-swift-toolkit) 200.4 (or newer)
* Xcode 15.0 (or newer)

The *ArcGIS Maps SDK for Swift Samples app* has a *Target SDK* version of *15.0*, meaning that it can run on devices with *iOS 15.0* or newer.
Expand All @@ -21,6 +35,9 @@ The *ArcGIS Maps SDK for Swift Samples app* has a *Target SDK* version of *15.0*
## Configuring API Keys

> [!IMPORTANT]
> Acquire the keys from your [dashboard](https://developers.arcgis.com/dashboard). Visit the developer's website to learn more about [API keys](https://developers.arcgis.com/documentation/mapping-apis-and-services/security/api-keys/).
To run this app and access specific, ready-to-use services such as basemap layer, follow the steps to add an API key to a secrets file stored in the project file's directory, `$(SRCROOT)/.secrets`.

1. Create a hidden secrets file in the project file's directory.
Expand All @@ -29,7 +46,7 @@ To run this app and access specific, ready-to-use services such as basemap layer
touch .secrets
```

2. Add your **API Key** to the secrets file aforementioned. Adding an API key allows you to access a set of ready-to-use services, including basemaps. Acquire the keys from your [dashboard](https://developers.arcgis.com/dashboard). Visit the developer's website to learn more about [API keys](https://developers.arcgis.com/documentation/mapping-apis-and-services/security/api-keys/).
2. Add your **API Key** to the aforementioned secrets file. Adding an API key allows you to access a set of ready-to-use services, including basemaps.

```sh
echo ARCGIS_API_KEY_IOS=your-api-key >> .secrets
Expand All @@ -54,7 +71,7 @@ Find a bug or want to request a new feature? Please let us know by [creating an

## Licensing

Copyright 2022 - 2023 Esri
Copyright 2022 - 2024 Esri

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
436 changes: 387 additions & 49 deletions Samples.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Samples.xcodeproj/xcshareddata/xcschemes/Samples.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
36 changes: 24 additions & 12 deletions Scripts/CI/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# A set of words that get omitted during letter-case checks.
# This set will be updated when a special word appears in a new sample.
exception_proper_nouns = {
'Arcade',
'ArcGIS Online',
'ArcGIS Pro',
'GeoPackage',
Expand All @@ -32,21 +33,19 @@
'Web Mercator'
}

# A set of category folder names for legacy support.
# A set of category folder names.
categories = {
'Maps',
'Layers',
'Features',
'Display information',
'Search',
'Edit data',
'Geometry',
'Route and directions',
'Analysis',
'Cloud and portal',
'Augmented Reality',
'Cloud and Portal',
'Edit and Manage Data',
'Layers',
'Maps',
'Scenes',
'Utility network',
'Augmented reality'
'Routing and Logistics',
'Search and Query',
'Utility Networks',
'Visualization'
}
# endregion

Expand Down Expand Up @@ -378,6 +377,19 @@ def flush_to_json_string(self) -> str:

return json.dumps(data, indent=4, sort_keys=True)

def check_category(self) -> None:
"""
Check if
1. metadata contains a category.
2. category is valid.
:return: None. Throws if exception occurs.
"""
if not self.category:
raise Exception(f'Error category - Missing category.')
elif self.category not in categories:
raise Exception(f'Error category - Invalid category - "{self.category}".')


class Readme:
essential_headers = {
Expand Down
6 changes: 6 additions & 0 deletions Scripts/CI/metadata_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ def run_check(path: str) -> None:
diff = '\n'.join(unified_diff(expected, actual))
raise Exception(f'Error inconsistent metadata - {path} - {diff}')

# 4. Check category.
try:
checker.check_category()
except Exception as err:
raise Exception(f'{checker.folder_path} - {err}')


def all_samples(path: str):
"""
Expand Down
154 changes: 78 additions & 76 deletions Scripts/DowloadPortalItemData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// A mapping of item IDs to filenames is maintained in the download directory.
// This mapping efficiently checks whether an item has already been downloaded.
// If an item already exists, it will skip that item.
// To delete and re-downloaded an item, remove its entry from the plist.

import Foundation

Expand All @@ -31,7 +32,7 @@ struct SampleDependency: Decodable {
}

/// A Portal Item and its data URL.
struct PortalItem {
struct PortalItem: Hashable {
static let arcGISOnlinePortalURL = URL(string: "https://www.arcgis.com")!

/// The identifier of the item.
Expand Down Expand Up @@ -125,61 +126,47 @@ func uncompressArchive(at sourceURL: URL, to destinationURL: URL) throws {
process.waitUntilExit()
}

/// Downloads file from portal and write the file(s) to appropriate path(s).
/// Downloads a file from a URL to a given location.
/// - Parameters:
/// - sourceURL: The portal URL to the resource.
/// - downloadDirectory: The directory that stores downloaded data.
/// - completion: A closure to handle the results.
func downloadFile(at sourceURL: URL, to downloadDirectory: URL, completion: @escaping (Result<URL, Error>) -> Void) {
let downloadTaskCompleted = { (temporaryURL: URL?, response: URLResponse?, error: Error?) in
if let temporaryURL = temporaryURL,
let response = response,
let suggestedFilename = response.suggestedFilename {
do {
let downloadName: String
let isArchive = (suggestedFilename as NSString).pathExtension == "zip"
// If the downloaded file is an archive and contains
// - 1 file, use the name of that file.
// - multiple files, use the suggested filename (*.zip).
// If it is not an archive, use the server suggested filename.
if isArchive {
let count = try count(ofFilesInArchiveAt: temporaryURL)
if count == 1 {
downloadName = try name(ofFileInArchiveAt: temporaryURL)
} else {
downloadName = suggestedFilename
}
} else {
downloadName = suggestedFilename
}

let downloadURL = downloadDirectory.appendingPathComponent(downloadName, isDirectory: false)

if FileManager.default.fileExists(atPath: downloadURL.path) {
try FileManager.default.removeItem(at: downloadURL)
}

if isArchive {
let extractURL = downloadURL.pathExtension == "zip"
// Uncompresses to directory named after archive.
? downloadURL.deletingPathExtension()
// Uncompresses to appropriate subdirectory.
: downloadURL.deletingLastPathComponent()
try uncompressArchive(at: temporaryURL, to: extractURL)
} else {
try FileManager.default.moveItem(at: temporaryURL, to: downloadURL)
}

completion(.success(downloadURL))
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
/// - downloadDirectory: The directory to store the downloaded data in.
/// - Throws: Exceptions when downloading, naming, uncompressing, and moving the file.
/// - Returns: The name of the downloaded file.
func downloadFile(from sourceURL: URL, to downloadDirectory: URL) async throws -> String? {
let (temporaryURL, response) = try await URLSession.shared.download(from: sourceURL)

guard let suggestedFilename = response.suggestedFilename else { return nil }
let isArchive = NSString(string: suggestedFilename).pathExtension == "zip"

let downloadName: String = try {
// If the downloaded file is an archive and contains
// - 1 file, use the name of that file.
// - multiple files, use the server suggested filename (*.zip).
// If it is not an archive, use the server suggested filename.
if isArchive,
try count(ofFilesInArchiveAt: temporaryURL) == 1 {
return try name(ofFileInArchiveAt: temporaryURL)
} else {
return suggestedFilename
}
}()
let downloadURL = downloadDirectory.appendingPathComponent(downloadName, isDirectory: false)

try? FileManager.default.removeItem(at: downloadURL)

if isArchive {
let extractURL = downloadURL.pathExtension == "zip"
// Uncompresses to directory named after archive.
? downloadURL.deletingPathExtension()
// Uncompresses to appropriate subdirectory.
: downloadURL.deletingLastPathComponent()

try uncompressArchive(at: temporaryURL, to: extractURL)
} else {
try FileManager.default.moveItem(at: temporaryURL, to: downloadURL)
}
let downloadTask = URLSession.shared.downloadTask(with: sourceURL, completionHandler: downloadTaskCompleted)
downloadTask.resume()

return downloadName
}

// MARK: Script Entry
Expand Down Expand Up @@ -207,7 +194,7 @@ if !FileManager.default.fileExists(atPath: downloadDirectoryURL.path) {
}

/// Portal Items created from iterating through all metadata's "offline\_data".
let portalItems: [PortalItem] = {
let portalItems: Set<PortalItem> = {
do {
// Finds all subdirectories under the root Samples directory.
let sampleSubDirectories = try FileManager.default
Expand All @@ -218,7 +205,7 @@ let portalItems: [PortalItem] = {
// Omit the decoding errors from samples that don't have dependencies.
let sampleDependencies = sampleJSONs
.compactMap { try? parseJSON(at: $0) }
return sampleDependencies.flatMap(\.offlineData)
return Set(sampleDependencies.lazy.flatMap(\.offlineData))
} catch {
print("error: Error decoding Samples dependencies: \(error.localizedDescription)")
exit(1)
Expand All @@ -241,39 +228,54 @@ let previousDownloadedItems: DownloadedItems = {
}()
var downloadedItems = previousDownloadedItems

// Asynchronously downloads portal items.
let dispatchGroup = DispatchGroup()

portalItems.forEach { portalItem in
let destinationURL = downloadDirectoryURL.appendingPathComponent(portalItem.identifier, isDirectory: true)
// Checks if a directory exists or not, to see if an item is already downloaded.
if FileManager.default.fileExists(atPath: destinationURL.path) {
print("info: Item \(portalItem.identifier) has already been downloaded.")
} else {
await withTaskGroup(of: Void.self) { group in
for portalItem in portalItems {
let destinationURL = downloadDirectoryURL.appendingPathComponent(
portalItem.identifier,
isDirectory: true
)

// Checks to see if an item needs downloading.
guard downloadedItems[portalItem.identifier] == nil ||
!FileManager.default.fileExists(atPath: destinationURL.path) else {
print("note: Item already downloaded: \(portalItem.identifier)")
continue
}

// Deletes the directory when the item is not in the plist.
try? FileManager.default.removeItem(at: destinationURL)

do {
// Creates an enclosing directory with portal item ID as its name.
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: false)
// Creates an enclosing directory with the portal item ID as its name.
try FileManager.default.createDirectory(
at: destinationURL,
withIntermediateDirectories: false
)
} catch {
print("error: Error creating download directory: \(error.localizedDescription).")
print("error: Error creating download directory: \(error.localizedDescription)")
exit(1)
}
print("info: Downloading item \(portalItem.identifier)")
fflush(stdout)
dispatchGroup.enter()
downloadFile(at: portalItem.dataURL, to: destinationURL) { result in
switch result {
case .success(let url):
downloadedItems[portalItem.identifier] = url.lastPathComponent
dispatchGroup.leave()
case .failure(let error):

group.addTask {
do {
guard let downloadName = try await downloadFile(
from: portalItem.dataURL,
to: destinationURL
) else { return }
print("note: Downloaded item: \(portalItem.identifier)")
fflush(stdout)

_ = await MainActor.run {
downloadedItems.updateValue(downloadName, forKey: portalItem.identifier)
}
} catch {
print("error: Error downloading item \(portalItem.identifier): \(error.localizedDescription)")
URLSession.shared.invalidateAndCancel()
exit(1)
}
}
}
}
dispatchGroup.wait()

// Updates the downloaded items property list record if needed.
if downloadedItems != previousDownloadedItems {
Expand Down
5 changes: 5 additions & 0 deletions Scripts/GenerateSampleViewSourceCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ private let arrayRepresentation = """
[
\(entries)
]
#if targetEnvironment(macCatalyst) || targetEnvironment(simulator)
// Exclude AR samples from Mac Catalyst and Simulator targets
// as they don't have camera and sensors available.
.filter { $0.category != "Augmented Reality" }
#endif
"""

do {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Favorites-bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Favorites-bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Favorites-icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

0 comments on commit 62fea7a

Please sign in to comment.