Overview
CloneableJSON extends JSON with data types such as images, locations, and coordinate mappings. Use it to extract, process, and save pole and midspan measurements. CloneableJSON stores flexible data inAnyCloneableData. Components use this wrapper to exchange data.
Reference
AnyCloneableDatais a type-erased container to hold complex data typesCloneableImagestores UIImage data with file persistence capabilitiesCloneableFileDataTypeis a protocol for file-based data types that have a UUID identifier
Pole measurement data structure
Pole measurements return this CloneableJSON structure:{
"high_accuracy": true,
"pole_information": {
"height": 35.5,
"lean_angle": 2.1,
"location": {
"latitude": 40.7128,
"longitude": -74.0060,
"altitude": 10.5
}
},
"attachments": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"type": "attachment",
"classification": "Transformer",
"inventoryClass": {
"name": "Transformer",
"subcategories": [
{
"name": "Size",
"value": "25 kVA"
},
{
"name": "Type",
"value": "Single Phase"
}
]
},
"owner": "Utility Company A",
"images": [
{
"imageId": "550e8400-e29b-41d4-a716-446655440002",
"point": {
"x": 256.5,
"y": 384.2
}
}
],
"related_task_id": "task_123",
"height": 28.5,
"distanceFromPoleTop": 7.0,
"relatedSpans": ["550e8400-e29b-41d4-a716-446655440010"],
"relatedGuys": ["550e8400-e29b-41d4-a716-446655440020"],
"heading": 180
}
],
"images": [
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"type": "pole",
"image": "[CloneableImage UUID reference]",
"referenceLine": {
"bottom": {
"id": "550e8400-e29b-41d4-a716-446655440004",
"pixelCoordinates": {
"x": 320.0,
"y": 800.0
},
"worldCoordinates": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"top": {
"id": "550e8400-e29b-41d4-a716-446655440005",
"pixelCoordinates": {
"x": 320.0,
"y": 100.0
},
"worldCoordinates": {
"x": 0.0,
"y": 0.0,
"z": 35.5
}
}
},
"scalingData": {
"pixelArray": [
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"pixelCoordinates": {
"x": 200.5,
"y": 450.2
},
"worldCoordinates": {
"x": -2.1,
"y": 0.8,
"z": 15.5
}
}
]
},
"cameraTilt": 5.2,
"predictedCameraTilt": 4.8,
"wasAutoCaptured": false,
"wasCapturedInSafeZone": true,
"stickDistance": 12.5,
"stickHeight": 8.0
}
]
}
Understand CloneableJSON
CloneableJSON uses subscripts that returnCloneableJSONValue enums. Accessor methods simplify data access.
Access values
Safe optional access
// Simple property access - returns nil if not found or wrong type
let height = result["pole_information"]["height"].numberValue
let classification = result["attachments"][0]["classification"].stringValue
let isHighAccuracy = result["high_accuracy"].boolValue
// With optional binding
if let height = result["pole_information"]["height"].numberValue {
print("Pole height: \(height) feet")
}
// Your original example, now simplified:
print("Vertical measurement result: \(result["pole_information"]["height"].numberValue ?? 0)")
Throwing access
// Throws error with details if wrong type or not found
do {
let height = try result["pole_information"]["height"].getNumber()
let classification = try result["attachments"][0]["classification"].getString()
let attachments = try result["attachments"].getArray()
print("Pole height: \(height) feet")
print("Found \(attachments.count) attachments")
} catch let error as JSONError {
switch error {
case .wrongType(let expected, let actual):
print("Type mismatch: expected \(expected) but found \(actual)")
case .valueNotFound(let path):
print("Value not found at path: \(path)")
default:
print("JSON Error: \(error.localizedDescription)")
}
}
Direct enum pattern matching
// Access returns CloneableJSONValue enum
let heightValue = result["pole_information"]["height"]
// Extract the actual value
switch heightValue {
case .value(let anyCloneableData):
// Now you can use the getter methods on AnyCloneableData
let numberValue = anyCloneableData.getNumberValue()
let stringValue = anyCloneableData.getStringValue()
let imageValue = anyCloneableData.getImageValue() // For CloneableImage
case .object(let dict):
// It's a nested object - dict is [String: CloneableJSONValue]
case .array(let array):
// It's an array - array is [CloneableJSONValue]
case .undefined:
// Key doesn't exist or value is null
}
Accessor properties and methods
Optional Properties (return nil if wrong type):
.numberValue→Double?.stringValue→String?.boolValue→Bool?.dateValue→Date?.objectValue→[String: CloneableJSONValue]?.arrayValue→[CloneableJSONValue]?.isUndefined→Bool(true if value is undefined)
Throwing Methods (throw JSONError if wrong type):
.getNumber()→Double.getString()→String.getBool()→Bool.getDate()→Date.getObject()→[String: CloneableJSONValue].getArray()→[CloneableJSONValue]
Helper Properties:
.typeDescription→String(returns current type: “object”, “array”, “string”, “number”, etc.)
JSONError Types
public enum JSONError: Error, LocalizedError {
case invalidJSON
case unsupportedType
case typeMismatch
case invalidSchema
case invalidRootStructure
case valueNotFound(path: String)
case wrongType(expected: String, actual: String)
}
Work with attachments
Extract attachment data
func extractAttachments(from result: CloneableJSON) -> [AttachmentData] {
var attachments: [AttachmentData] = []
// Get attachments array using convenient accessor
guard let attachmentsArray = result["attachments"].arrayValue else {
print("No attachments found")
return attachments
}
for attachmentValue in attachmentsArray {
var attachment = AttachmentData()
// Extract basic properties using optional accessors
attachment.id = attachmentValue["id"].stringValue ?? UUID().uuidString
attachment.type = attachmentValue["type"].stringValue ?? "unknown"
attachment.classification = attachmentValue["classification"].stringValue
attachment.height = attachmentValue["height"].numberValue
attachment.distanceFromTop = attachmentValue["distanceFromPoleTop"].numberValue
attachment.owner = attachmentValue["owner"].stringValue
attachment.relatedTaskId = attachmentValue["related_task_id"].stringValue
attachment.heading = attachmentValue["heading"].numberValue
// Extract inventory classification with subcategories
if attachmentValue["inventoryClass"].objectValue != nil {
attachment.inventoryData = extractInventoryClassification(attachmentValue["inventoryClass"])
}
// Extract related spans (array of UUIDs)
if let relatedSpans = attachmentValue["relatedSpans"].arrayValue {
attachment.relatedSpanIds = relatedSpans.compactMap { $0.stringValue }
}
// Extract related guys (array of UUIDs)
if let relatedGuys = attachmentValue["relatedGuys"].arrayValue {
attachment.relatedGuyIds = relatedGuys.compactMap { $0.stringValue }
}
// Extract image measurements (pixel points on images)
if let imagesArray = attachmentValue["images"].arrayValue {
attachment.imageReferences = extractAttachmentImageReferences(imagesArray)
}
attachments.append(attachment)
}
return attachments
}
struct AttachmentData {
var id: String = ""
var type: String = "" // "attachment", "guy", "wire"
var classification: String? // Equipment name
var height: Double? // Height above ground in feet
var distanceFromTop: Double? // Distance from pole top in feet
var owner: String? // Utility company
var relatedTaskId: String? // Related verification task
var heading: Double? // Compass direction in degrees
var inventoryData: InventoryClassificationData?
var relatedSpanIds: [String] = [] // Related power line span UUIDs
var relatedGuyIds: [String] = [] // Related guy wire UUIDs
var imageReferences: [AttachmentImageReference] = []
}
// Helper function to extract attachment image references
func extractAttachmentImageReferences(_ imagesArray: [CloneableJSONValue]) -> [AttachmentImageReference] {
var references: [AttachmentImageReference] = []
for imageValue in imagesArray {
var ref = AttachmentImageReference()
// Extract image ID
ref.imageId = imageValue["imageId"].stringValue ?? ""
// Extract pixel point where attachment was marked
if let x = imageValue["point"]["x"].numberValue,
let y = imageValue["point"]["y"].numberValue {
ref.pixelPoint = CGPoint(x: x, y: y)
}
references.append(ref)
}
return references
}
struct AttachmentImageReference {
var imageId: String = ""
var pixelPoint: CGPoint = CGPoint.zero // Where attachment was marked on image
}
Extract inventory classifications
func extractInventoryClassification(_ inventoryClass: CloneableJSONValue) -> InventoryClassificationData {
var classification = InventoryClassificationData()
// Extract main classification name
classification.name = inventoryClass["name"].stringValue ?? "Unknown"
// Extract subcategories
if let subcategoriesArray = inventoryClass["subcategories"].arrayValue {
for subcategoryValue in subcategoriesArray {
var subcategory = SubcategoryData(name: "", value: nil, required: false, options: [])
subcategory.name = subcategoryValue["name"].stringValue ?? ""
subcategory.value = subcategoryValue["value"].stringValue
subcategory.required = subcategoryValue["required"].boolValue ?? false
// Extract available options
if let optionsArray = subcategoryValue["options"].arrayValue {
subcategory.options = optionsArray.compactMap { $0.stringValue }
}
classification.subcategories.append(subcategory)
}
}
return classification
}
struct InventoryClassificationData {
var name: String = ""
var subcategories: [SubcategoryData] = []
}
struct SubcategoryData {
var name: String
var value: String?
var required: Bool
var options: [String]
}
Work with images
Extract and save images
import UIKit
func extractAndSaveImages(from result: CloneableJSON, to directory: URL) async {
guard let imagesArray = result["images"].arrayValue else {
print("No images found")
return
}
for imageValue in imagesArray {
let imageId = imageValue["id"].stringValue ?? "unknown"
// Extract CloneableImage using pattern matching
var cloneableImage: CloneableImage?
if case .value(let imageData) = imageValue["image"] {
cloneableImage = imageData.data as? CloneableImage
}
guard let image = cloneableImage else {
print("Could not extract CloneableImage for \(imageId)")
continue
}
// Get the UIImage from CloneableImage
guard let uiImage = image.imageData else {
print("CloneableImage has no imageData for \(imageId)")
continue
}
// Save image with metadata
await saveImageWithMetadata(
image: uiImage,
imageId: imageId,
cloneableImage: image,
metadata: imageValue,
to: directory
)
// Extract and save scaling data
await extractScalingData(imageValue: imageValue, imageId: imageId, to: directory)
}
}
func saveImageWithMetadata(
image: UIImage,
imageId: String,
cloneableImage: CloneableImage,
metadata: CloneableJSONValue,
to directory: URL
) async {
// Save the image file using CloneableImage's built-in method
let imageURL = directory.appendingPathComponent("\(imageId).jpg")
do {
try cloneableImage.saveToFile(fileURL: imageURL)
print("Saved image: \(imageURL.path)")
} catch {
print("Failed to save image \(imageId): \(error)")
}
// Save image metadata
let metadataDict = extractImageMetadata(metadata)
let metadataURL = directory.appendingPathComponent("\(imageId)_metadata.json")
do {
let jsonData = try JSONSerialization.data(withJSONObject: metadataDict, options: [.prettyPrinted])
try jsonData.write(to: metadataURL)
print("Saved image metadata: \(metadataURL.path)")
} catch {
print("Failed to save image metadata: \(error)")
}
}
func extractImageMetadata(_ imageValue: CloneableJSONValue) -> [String: Any] {
var metadata: [String: Any] = [:]
// Extract basic properties using convenient accessors
if let id = imageValue["id"].stringValue { metadata["id"] = id }
if let type = imageValue["type"].stringValue { metadata["type"] = type }
if let timestamp = imageValue["captureTimestamp"].stringValue { metadata["captureTimestamp"] = timestamp }
if let orientation = imageValue["deviceOrientation"].stringValue { metadata["deviceOrientation"] = orientation }
// Extract resolution
if let width = imageValue["imageResolution"]["width"].numberValue,
let height = imageValue["imageResolution"]["height"].numberValue {
metadata["imageResolution"] = [
"width": width,
"height": height
]
}
// Extract AR camera tilt data
if let tilt = imageValue["cameraTilt"].numberValue { metadata["cameraTilt"] = tilt }
if let predictedTilt = imageValue["predictedCameraTilt"].numberValue {
metadata["predictedCameraTilt"] = predictedTilt
}
// Extract capture method flags
if let autoCaptured = imageValue["wasAutoCaptured"].boolValue {
metadata["wasAutoCaptured"] = autoCaptured
}
if let safeZone = imageValue["wasCapturedInSafeZone"].boolValue {
metadata["wasCapturedInSafeZone"] = safeZone
}
// Extract AR accuracy measurements
if let distance = imageValue["stickDistance"].numberValue { metadata["stickDistance"] = distance }
if let height = imageValue["stickHeight"].numberValue { metadata["stickHeight"] = height }
return metadata
}
Use the AnyCloneableData extension
// Using the AnyCloneableData extension method
func extractImageUsingExtension(from imageValue: CloneableJSONValue) -> UIImage? {
if case .value(let anyCloneableData) = imageValue["image"] {
return anyCloneableData.getImageValue() // Returns UIImage?
}
return nil
}
// Example usage
if let uiImage = extractImageUsingExtension(from: imageValue) {
// Work directly with UIImage
let imageData = uiImage.jpegData(compressionQuality: 0.8)
// ... save or process the image
}
Work with scaling data
func extractScalingData(imageValue: CloneableJSONValue, imageId: String, to directory: URL) async {
var scalingData: [String: Any] = [
"imageId": imageId,
"timestamp": ISO8601DateFormatter().string(from: Date())
]
// Extract pixel array scaling data
if let pixelArray = imageValue["scalingData"]["pixelArray"].arrayValue {
var scalingPoints: [[String: Any]] = []
for pixelPoint in pixelArray {
var scalingPoint: [String: Any] = [:]
if let id = pixelPoint["id"].stringValue { scalingPoint["id"] = id }
// Extract pixel coordinates
if let x = pixelPoint["pixelCoordinates"]["x"].numberValue,
let y = pixelPoint["pixelCoordinates"]["y"].numberValue {
scalingPoint["pixelCoordinates"] = ["x": x, "y": y]
}
// Extract world coordinates
if let x = pixelPoint["worldCoordinates"]["x"].numberValue,
let y = pixelPoint["worldCoordinates"]["y"].numberValue,
let z = pixelPoint["worldCoordinates"]["z"].numberValue {
scalingPoint["worldCoordinates"] = ["x": x, "y": y, "z": z]
}
scalingPoints.append(scalingPoint)
}
scalingData["pixelArray"] = scalingPoints
}
// Extract reference line data
if imageValue["referenceLine"].objectValue != nil {
scalingData["referenceLine"] = extractReferenceLineData(imageValue["referenceLine"])
}
// Save scaling data to JSON file
let scalingDataURL = directory.appendingPathComponent("\(imageId)_scaling.json")
do {
let jsonData = try JSONSerialization.data(withJSONObject: scalingData, options: [.prettyPrinted, .sortedKeys])
try jsonData.write(to: scalingDataURL)
print("Saved scaling data: \(scalingDataURL.path)")
} catch {
print("Failed to save scaling data for \(imageId): \(error)")
}
}
func extractReferenceLineData(_ referenceLine: CloneableJSONValue) -> [String: Any] {
var referenceLineData: [String: Any] = [:]
// Extract bottom point
if referenceLine["bottom"].objectValue != nil {
referenceLineData["bottom"] = extractPointData(referenceLine["bottom"])
}
// Extract top point
if referenceLine["top"].objectValue != nil {
referenceLineData["top"] = extractPointData(referenceLine["top"])
}
return referenceLineData
}
func extractPointData(_ point: CloneableJSONValue) -> [String: Any] {
var pointData: [String: Any] = [:]
if let id = point["id"].stringValue { pointData["id"] = id }
// Extract pixel coordinates
if let x = point["pixelCoordinates"]["x"].numberValue,
let y = point["pixelCoordinates"]["y"].numberValue {
pointData["pixelCoordinates"] = ["x": x, "y": y]
}
// Extract world coordinates
if let x = point["worldCoordinates"]["x"].numberValue,
let y = point["worldCoordinates"]["y"].numberValue,
let z = point["worldCoordinates"]["z"].numberValue {
pointData["worldCoordinates"] = ["x": x, "y": y, "z": z]
}
return pointData
}
Work with CloneableImage directly
Advanced image operations
// Download image from backend if needed
func downloadImageIfNeeded(_ cloneableImage: CloneableImage) async -> CloneableImage? {
// Check if image data is already loaded
if cloneableImage.imageData != nil {
return cloneableImage
}
// Download from backend
do {
let downloadedImage = try await cloneableImage.downloadImage(
maxRetries: 3,
onProgress: { progress in
print("Download progress: \(Int(progress * 100))%")
}
)
return downloadedImage
} catch {
print("Failed to download image: \(error)")
return nil
}
}
// Save to temporary file
func saveImageTemporarily(_ cloneableImage: CloneableImage) throws -> URL {
return try cloneableImage.saveToTemporaryFile(fileExtension: "jpg")
}
// Create CloneableImage from UIImage
func createCloneableImage(from uiImage: UIImage, persistTemporary: Bool = false) -> CloneableImage {
return CloneableImage(uiImage, persistTemporary: persistTemporary)
}
Export a JSON string
CloneableJSON provides a built-in method to convert to standard JSON string:// Convert CloneableJSON to pretty-printed JSON string
let jsonString = cloneableJSON.getJSONValue()
// Example usage with pole measurement
let poleResult: CloneableJSON = // ... your measurement result
let jsonOutput = poleResult.getJSONValue()
// Use the JSON string
print(jsonOutput) // Print to console
saveToFile(jsonOutput) // Save to file
sendToAPI(jsonOutput) // Send to web API
// Save to file example
func saveMeasurementResult(_ result: CloneableJSON, to url: URL) throws {
let jsonString = result.getJSONValue()
try jsonString.write(to: url, atomically: true, encoding: .utf8)
}
getJSONValue() method automatically:
- Converts
CloneableImageobjects to UUID strings (viagetJSONValue()method) - Converts
CloneableFileDataTypeobjects to UUID strings - Converts
CloneableNumberto standard JSON numbers - Returns pretty-printed, sorted JSON string
- Preserves complete nested data structure
- Handles undefined values as
null
Schema support
CloneableJSON supports optional schema validation:// Check if JSON has a schema
if let schema = cloneableJSON.schema {
print("Schema ID: \(schema.id)")
print("Schema name: \(schema.name)")
// Validate against schema
if cloneableJSON.validate() {
print("JSON validates against schema")
} else {
print("JSON validation failed")
}
}
// Access schema properties
if let heightSchema = cloneableJSON[schemaProperty: "height"] {
print("Height type: \(heightSchema.type)")
print("Height description: \(heightSchema.description)")
}
Get all file data values
// Get all file data values from the JSON (includes images)
let fileDataValues = cloneableJSON.getAllFileDataValues()
for fileData in fileDataValues {
if let cloneableImage = fileData.data as? CloneableImage {
print("Found image: \(cloneableImage.id)")
if let uiImage = cloneableImage.imageData {
print(" Image size: \(uiImage.size)")
} else {
print(" Image not loaded, needs download")
}
} else if let fileDataType = fileData.data as? CloneableFileDataType {
print("Found file: \(fileDataType.id)")
}
}
Error handling best practices
When using the throwing accessor methods, you can handle specific error cases:do {
// Chain multiple accesses with clear error context
let height = try result["pole_information"]["height"].getNumber()
let attachments = try result["attachments"].getArray()
for (index, attachment) in attachments.enumerated() {
do {
let classification = try attachment["classification"].getString()
print("Attachment \(index): \(classification)")
} catch {
print("Attachment \(index) has no classification")
}
}
} catch let error as JSONError {
switch error {
case .wrongType(let expected, let actual):
print("Type mismatch: expected \(expected) but found \(actual)")
case .valueNotFound(let path):
print("Value not found at path: \(path)")
default:
print("JSON Error: \(error.localizedDescription)")
}
} catch {
print("Unexpected error: \(error)")
}
Best practices
- Use optional accessors for exploratory code - When you’re not sure if a value exists or what type it is
- Use throwing accessors for production code - When you expect certain data to be present and want clear error messages
- Check
.isUndefinedbefore accessing nested values - To avoid unnecessary operations - Use
.typeDescriptionfor debugging - To understand what type of value you’re dealing with - Prefer the convenience accessors - They’re cleaner and more Swift-like than manual enum matching
- Handle CloneableImage download scenarios - Images may need to be downloaded from backend
- Use CloneableImage’s built-in file operations - For saving and loading images efficiently
- Import UIKit when working with images - CloneableImage uses UIImage which requires UIKit
Quick reference
Your Original Problem - Now Solved!
// Before (complex):
if case .value(let cloneableData) = result["pole_information"]["height"],
let numberValue = cloneableData.getNumberValue() {
print("Vertical measurement result: \(numberValue)")
}
// After (simple):
print("Vertical measurement result: \(result["pole_information"]["height"].numberValue ?? 0)")
// Or with error handling:
do {
let height = try result["pole_information"]["height"].getNumber()
print("Vertical measurement result: \(height)")
} catch {
print("Could not get height: \(error)")
}