Attachment Management

Comprehensive guide to using the Zync SDK’s attachment management features.

Table of contents

  1. Adding Attachments
    1. Add New Attachment
    2. Supported File URIs
  2. Updating Attachments
    1. Rename Attachment
    2. Update Description
    3. Update File
  3. Retrieving Attachments
    1. Get Single Attachment
    2. Get Paginated Attachments
    3. Get Filtered Attachments
    4. Get Attachments Grouped by Date
  4. Tag Management
    1. Add Tags to Attachments
    2. Remove Tags from Attachments
    3. Bulk Tag Operations
      1. Performance Benefits
    4. Working with Attachment Tags
    5. Get Tags for Module
  5. Real-time Updates
    1. Observe Attachment Changes
  6. Deleting Attachments
    1. Delete Attachment
    2. Delete Multiple Attachments
  7. Error Handling
    1. Common Error Types
  8. Immediate Upload
    1. Upload Multiple Files Immediately
    2. Upload Single File Immediately
  9. Best Practices
    1. 1. File URI Validation
    2. 2. Use Real-time Updates
  10. Supported Modules

The AttachmentManager provides offline-first attachment management with automatic sync capabilities, including comprehensive tag management for organizing and categorizing attachments.

Adding Attachments

Add New Attachment

try {
    val result = zync.attachments.addAttachment(
        filePath = "file:///storage/emulated/0/Pictures/photo.jpg", // File URI
        attachmentName = "Photo of equipment",
        module = "job",
        moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        type = AttachmentType.NOTE,
        description = "Equipment damage photo",
        tagUids = listOf("urgent-tag-uid", "quality-check-uid") // Optional tags
    )
    
    when {
        result.isSuccess -> {
            val attachment = result.getOrNull()
            println("Added attachment: ${attachment?.name}")
        }
        result.isFailure -> {
            val error = result.exceptionOrNull()
            println("Failed to add attachment: ${error?.message}")
        }
    }
} catch (e: Exception) {
    println("Error: ${e.message}")
}
do {
    let result = try await zync.attachments.addAttachment(
        filePath: "file:///var/mobile/Documents/photo.jpg", // File URI
        attachmentName: "Photo of equipment",
        module: "job",
        moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        type: .note,
        description: "Equipment damage photo",
        tagUids: ["urgent-tag-uid", "quality-check-uid"] // Optional tags
    )
    
    switch result {
    case .success(let attachment):
        print("Added attachment: \(attachment.name)")
    case .failure(let error):
        print("Failed to add attachment: \(error.localizedDescription)")
    }
} catch {
    print("Error: \(error.localizedDescription)")
}

Supported File URIs

The SDK accepts these URI formats:

  • File URIs: file:///path/to/file.jpg
  • Content URIs (Android only): content://media/picker/0/...

Important: Plain file system paths without URI schemes are not accepted. Always use proper URI formats.

Updating Attachments

Rename Attachment

val result = zync.attachments.renameAttachment(
    attachmentUid = "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newName = "Updated equipment photo"
)

result.fold(
    onSuccess = { attachment ->
        println("Renamed to: ${attachment.name}")
    },
    onFailure = { error ->
        println("Rename failed: ${error.message}")
    }
)
let result = try await zync.attachments.renameAttachment(
    attachmentUid: "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newName: "Updated equipment photo"
)

switch result {
case .success(let attachment):
    print("Renamed to: \(attachment.name)")
case .failure(let error):
    print("Rename failed: \(error.localizedDescription)")
}

Update Description

val result = zync.attachments.updateAttachmentDescription(
    attachmentUid = "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newDescription = "Updated description with more details"
)

when (result) {
    is AttachmentOperationResult.Success -> {
        val updatedAttachment = result.data
        println("Description updated: ${updatedAttachment.description}")
    }
    is AttachmentOperationResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
let result = try await zync.attachments.updateAttachmentDescription(
    attachmentUid: "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newDescription: "Updated description with more details"
)

switch onEnum(of: result) {
case .success(let success):
    let updatedAttachment = success.data
    print("Description updated: \(updatedAttachment.description ?? "")")
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Update File

val result = zync.attachments.updateAttachmentFile(
    attachmentUid = "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newFilePath = "file:///storage/emulated/0/Pictures/new_photo.jpg"
)

when (result) {
    is AttachmentOperationResult.Success -> {
        val updatedAttachment = result.data
        println("File updated: ${updatedAttachment.name}")
    }
    is AttachmentOperationResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
let result = try await zync.attachments.updateAttachmentFile(
    attachmentUid: "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    newFilePath: "file:///var/mobile/Documents/new_photo.jpg"
)

switch onEnum(of: result) {
case .success(let success):
    let updatedAttachment = success.data
    print("File updated: \(updatedAttachment.name)")
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Retrieving Attachments

Get Single Attachment

val attachment = zync.attachments.getAttachment("9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f")
if (attachment != null) {
    println("Found: ${attachment.name}")
} else {
    println("Attachment not found")
}
if let attachment = try await zync.attachments.getAttachment("9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f") {
    print("Found: \(attachment.name)")
} else {
    print("Attachment not found")
}

Get Paginated Attachments

val result = zync.attachments.getAttachments(
    module = ZyncModule.JOB,
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page = 1,
    pageSize = 20
)

when (result) {
    is AttachmentResult.Success -> {
        val attachments = result.attachments
        println("Loaded ${attachments.size} attachments")
        println("Page ${result.currentPage} of ${result.totalPages}")
        println("Total: ${result.totalCount}, Has more: ${result.hasMore}")
        
        attachments.forEach { attachment ->
            println("- ${attachment.attachmentName} (${attachment.type?.const})")
        }
    }
    is AttachmentResult.Failure -> {
        println("Error: ${result.error.message}")
        // Handle different error types if needed
        when (result.error) {
            is ZyncError.Network -> println("Check your internet connection")
            is ZyncError.Error -> {
                // General error with optional code
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
let result = try await zync.attachments.getAttachments(
    module: .job,
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page: 1,
    pageSize: 20
)

switch onEnum(of: result) {
case .success(let success):
    print("Loaded \(success.attachments.count) attachments")
    print("Page \(success.currentPage) of \(success.totalPages)")
    print("Total: \(success.totalCount), Has more: \(success.hasMore)")
    
    for attachment in success.attachments {
        print("- \(attachment.attachmentName) (\(attachment.type?.const ?? "unknown"))")
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
    // Handle different error types if needed
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Get Filtered Attachments

You can filter attachments by date range, media type, and tags. Multiple filters can be combined, and when multiple tags are specified, only attachments that have ALL the specified tags will be returned (AND logic):

import co.zuper.mobile.sync.sdk.public.attachments.models.AttachmentFilters
import co.zuper.mobile.sync.sdk.public.attachments.models.AttachmentMediaType
import kotlinx.datetime.*

// Create date filters using date format (YYYY-MM-DD)
val startOfWeek = Clock.System.now().minus(7.days).toString().substringBefore('T')
val endOfDay = Clock.System.now().toString().substringBefore('T')

// Filter for images from the past week
val filters = AttachmentFilters(
    startDate = startOfWeek, // Date format: "2023-12-25"
    endDate = endOfDay,      // Date format: "2023-12-25"
    mediaType = AttachmentMediaType.IMAGE
)

// Filter by single tag - only attachments with "urgent" tag
val tagFilters = AttachmentFilters(
    tags = listOf("d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f") // urgent tag UID
)

// Filter by multiple tags - only attachments that have ALL specified tags (AND logic)
val multipleTagFilters = AttachmentFilters(
    tags = listOf(
        "d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f", // urgent tag
        "f47c8a1e-4c9b-2d3e-8f7g-5h6i7j8k9l0m", // quality-check tag
        "a12b3c4d-5e6f-7890-abcd-ef1234567890"  // compliance tag
    )
)

// Combined filtering - images from past week with urgent and quality-check tags
val combinedFilters = AttachmentFilters(
    startDate = startOfWeek, // Date format: "2023-12-25"
    endDate = endOfDay,      // Date format: "2023-12-25"
    mediaType = AttachmentMediaType.IMAGE,
    tags = listOf(
        "d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f", // urgent tag
        "f47c8a1e-4c9b-2d3e-8f7g-5h6i7j8k9l0m"  // quality-check tag
    )
)

val result = zync.attachments.getAttachments(
    module = ZyncModule.JOB,
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page = 1,
    pageSize = 20,
    filters = combinedFilters // Use any of the filter examples above
)

when (result) {
    is AttachmentResult.Success -> {
        println("Found ${result.attachments.size} image attachments from the past week")
        println("Page ${result.currentPage} of ${result.totalPages}")
        result.attachments.forEach { attachment ->
            println("- ${attachment.attachmentName} (${attachment.mimeType})")
        }
    }
    is AttachmentResult.Failure -> {
        println("Error: ${result.error.message}")
    }
}
import Foundation

// Create date filters using date format (YYYY-MM-DD)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let startOfWeek = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
let endOfDay = Date()

// Filter for images from the past week
let filters = AttachmentFilters(
    startDate: dateFormatter.string(from: startOfWeek), // Date format: "2023-12-25"
    endDate: dateFormatter.string(from: endOfDay),      // Date format: "2023-12-25"
    mediaType: .image
)

// Filter by single tag - only attachments with "urgent" tag
let tagFilters = AttachmentFilters(
    tags: ["d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f"] // urgent tag UID
)

// Filter by multiple tags - only attachments that have ALL specified tags (AND logic)
let multipleTagFilters = AttachmentFilters(
    tags: [
        "d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f", // urgent tag
        "f47c8a1e-4c9b-2d3e-8f7g-5h6i7j8k9l0m", // quality-check tag
        "a12b3c4d-5e6f-7890-abcd-ef1234567890"  // compliance tag
    ]
)

// Combined filtering - images from past week with urgent and quality-check tags
let combinedFilters = AttachmentFilters(
    startDate: dateFormatter.string(from: startOfWeek), // Date format: "2023-12-25"
    endDate: dateFormatter.string(from: endOfDay),      // Date format: "2023-12-25"
    mediaType: .image,
    tags: [
        "d47c8f1e-2a9b-4c3d-8e7f-1a2b3c4d5e6f", // urgent tag
        "f47c8a1e-4c9b-2d3e-8f7g-5h6i7j8k9l0m"  // quality-check tag
    ]
)

let result = try await zync.attachments.getAttachments(
    module: .job,
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page: 1,
    pageSize: 20,
    filters: combinedFilters // Use any of the filter examples above
)

switch onEnum(of: result) {
case .success(let success):
    print("Found \(success.attachments.count) image attachments from the past week")
    print("Page \(success.currentPage) of \(success.totalPages)")
    for attachment in success.attachments {
        print("- \(attachment.attachmentName) (\(attachment.mimeType ?? "unknown"))")
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
}

Get Attachments Grouped by Date

Get attachments organized by creation date with human-readable labels:

// Get all attachments grouped by date
val result = zync.attachments.getAttachmentsGroupedByDate(
    module = ZyncModule.JOB,
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page = 1,
    pageSize = 20
)

// Or with filters - get only images from past month grouped by date
val startOfMonth = Clock.System.now().minus(30.days).toString().substringBefore('T')
val endOfDay = Clock.System.now().toString().substringBefore('T')
val filters = AttachmentFilters(
    startDate = startOfMonth, // Date format: "2023-12-25"
    endDate = endOfDay,       // Date format: "2023-12-25"
    mediaType = AttachmentMediaType.IMAGE
)

val filteredResult = zync.attachments.getAttachmentsGroupedByDate(
    module = ZyncModule.JOB,
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page = 1,
    pageSize = 20,
    filters = filters
)

when (result) {
    is AttachmentGroupResult.Success -> {
        val groups = result.groups
        println("Loaded ${groups.size} date groups")
        
        groups.forEach { group ->
            println("📅 ${group.groupLabel} (${group.date})")
            group.attachments.forEach { attachment ->
                println("  - ${attachment.attachmentName} (${attachment.type?.const})")
            }
        }
        
        // Pagination info
        println("Page ${result.currentPage} of ${result.totalPages}")
        println("Total attachments: ${result.totalCount}")
        println("Has more: ${result.hasMore}")
    }
    is AttachmentGroupResult.Failure -> {
        println("Error: ${result.error.message}")
        // Handle different error types if needed
        when (result.error) {
            is ZyncError.Network -> println("Check your internet connection")
            is ZyncError.Error -> {
                // General error with optional code
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
// Get all attachments grouped by date
let result = try await zync.attachments.getAttachmentsGroupedByDate(
    module: .job,
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page: 1,
    pageSize: 20
)

// Or with filters - get only videos from past month grouped by date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let pastMonth = Calendar.current.date(byAdding: .day, value: -30, to: Date()) ?? Date()
let now = Date()
let filters = AttachmentFilters(
    startDate: dateFormatter.string(from: pastMonth), // Date format: "2023-12-25"
    endDate: dateFormatter.string(from: now),         // Date format: "2023-12-25"
    mediaType: .video
)

let filteredResult = try await zync.attachments.getAttachmentsGroupedByDate(
    module: .job,
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page: 1,
    pageSize: 20,
    filters: filters
)

switch onEnum(of: result) {
case .success(let success):
    print("Loaded \(success.groups.count) date groups")
    
    for group in success.groups {
        print("📅 \(group.groupLabel) (\(group.date))")
        for attachment in group.attachments {
            print("  - \(attachment.attachmentName) (\(attachment.type?.const ?? "unknown"))")
        }
    }
    
    // Pagination info
    print("Page \(success.currentPage) of \(success.totalPages)")
    print("Total attachments: \(success.totalCount)")
    print("Has more: \(success.hasMore)")
case .failure(let failure):
    print("Error: \(failure.error.message)")
    // Handle different error types if needed
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Tag Management

Add Tags to Attachments

You can add tags to existing attachments to help organize and categorize them:

// Add a single tag to an attachment
val result = zync.attachments.addTag(
    attachmentUid = "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tagUid = "urgent-priority-tag"
)

result.fold(
    onSuccess = { success ->
        if (success) {
            println("Tag added successfully")
        } else {
            println("Failed to add tag")
        }
    },
    onFailure = { error ->
        when (error) {
            is AttachmentError.AttachmentNotFound -> {
                println("Attachment not found: ${error.message}")
            }
            is AttachmentError.TagNotFound -> {
                println("Tag not found: ${error.message}")
            }
            is AttachmentError.TagAlreadyExists -> {
                println("Tag already associated: ${error.message}")
            }
            else -> {
                println("Error: ${error.message}")
            }
        }
    }
)
// Add a single tag to an attachment
let result = try await zync.attachments.addTag(
    attachmentUid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tagUid: "urgent-priority-tag"
)

switch result {
case .success(let success):
    if success {
        print("Tag added successfully")
    } else {
        print("Failed to add tag")
    }
case .failure(let error):
    switch error {
    case AttachmentError.attachmentNotFound(let message):
        print("Attachment not found: \(message)")
    case AttachmentError.tagNotFound(let message):
        print("Tag not found: \(message)")
    case AttachmentError.tagAlreadyExists(let message):
        print("Tag already associated: \(message)")
    default:
        print("Error: \(error.localizedDescription)")
    }
}

Remove Tags from Attachments

Remove tags that are no longer relevant:

// Remove a tag from an attachment
val result = zync.attachments.removeTag(
    attachmentUid = "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tagUid = "urgent-priority-tag"
)

result.fold(
    onSuccess = { success ->
        if (success) {
            println("Tag removed successfully")
        } else {
            println("Failed to remove tag")
        }
    },
    onFailure = { error ->
        when (error) {
            is AttachmentError.AttachmentNotFound -> {
                println("Attachment not found: ${error.message}")
            }
            is AttachmentError.TagNotFound -> {
                println("Tag not associated with attachment: ${error.message}")
            }
            else -> {
                println("Error: ${error.message}")
            }
        }
    }
)
// Remove a tag from an attachment
let result = try await zync.attachments.removeTag(
    attachmentUid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tagUid: "urgent-priority-tag"
)

switch result {
case .success(let success):
    if success {
        print("Tag removed successfully")
    } else {
        print("Failed to remove tag")
    }
case .failure(let error):
    switch error {
    case AttachmentError.attachmentNotFound(let message):
        print("Attachment not found: \(message)")
    case AttachmentError.tagNotFound(let message):
        print("Tag not associated with attachment: \(message)")
    default:
        print("Error: \(error.localizedDescription)")
    }
}

Bulk Tag Operations

For better performance when working with multiple tags, use the bulk operations instead of individual calls:

// Bulk add multiple tags to an existing attachment - RECOMMENDED
val result = zync.attachments.bulkAddTags(
    attachmentUid = "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tags = listOf(
        "priority-high-tag",
        "review-needed-tag", 
        "customer-visible-tag",
        "quality-check-tag"
    )
)

when (result) {
    is ZyncResult.Success -> {
        println("Successfully added ${tags.size} tags to attachment")
    }
    is ZyncResult.Failure -> {
        println("Failed to add tags: ${result.error.message}")
    }
}

// Create attachment with multiple tags during creation
val createResult = zync.attachments.addAttachment(
    filePath = "file:///storage/emulated/0/Pictures/inspection.jpg",
    attachmentName = "Equipment inspection photo",
    module = "job",
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    type = AttachmentType.NOTE,
    description = "Annual safety inspection",
    tagUids = listOf(
        "safety-inspection-tag",
        "annual-review-tag", 
        "equipment-condition-tag",
        "compliance-required-tag"
    )
)

// ⚠️ Less efficient - avoid for multiple tags
val tagUids = listOf("priority-high", "review-needed", "customer-visible")
tagUids.forEach { tagUid ->
    zync.attachments.addTag(attachmentUid, tagUid)
}
// Bulk add multiple tags to an existing attachment - RECOMMENDED
let result = try await zync.attachments.bulkAddTags(
    attachmentUid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    tags: [
        "priority-high-tag",
        "review-needed-tag", 
        "customer-visible-tag",
        "quality-check-tag"
    ]
)

switch onEnum(of: result) {
case .success:
    print("Successfully added \(tags.count) tags to attachment")
case .failure(let failure):
    print("Failed to add tags: \(failure.error.message)")
}

// Create attachment with multiple tags during creation
let createResult = try await zync.attachments.addAttachment(
    filePath: "file:///var/mobile/Documents/inspection.jpg",
    attachmentName: "Equipment inspection photo",
    module: "job",
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    type: .note,
    description: "Annual safety inspection",
    tagUids: [
        "safety-inspection-tag",
        "annual-review-tag", 
        "equipment-condition-tag",
        "compliance-required-tag"
    ]
)

// ⚠️ Less efficient - avoid for multiple tags
let tagUids = ["priority-high", "review-needed", "customer-visible"]
for tagUid in tagUids {
    let _ = try await zync.attachments.addTag(attachmentUid, tagUid)
}

Performance Benefits

Use bulkAddTags instead of multiple addTag calls when adding 2 or more tags:

  • Atomic Operation: All tags are added in a single database transaction
  • Optimized Sync: Creates a single CRUD operation instead of multiple separate operations
  • Better Performance: Reduces database overhead and improves sync efficiency
  • Smart Validation: Automatically filters out duplicate tags and validates all tags before processing

Working with Attachment Tags

When retrieving attachments, the tags are automatically included:

// Get attachment with tags
val attachment = zync.attachments.getAttachment("f47ac10b-58cc-4372-a567-0e02b2c3d479")
attachment?.let { att ->
    println("Attachment: ${att.attachmentName}")
    println("Tags (${att.tags.size}):")
    att.tags.forEach { tag ->
        println("  - ${tag.tagName} (${tag.tagColor ?: "no color"})")
    }
}

// Get all attachments with their tags
val result = zync.attachments.getAttachments(
    module = ZyncModule.JOB,
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page = 1,
    pageSize = 20
)

when (result) {
    is AttachmentResult.Success -> {
        result.attachments.forEach { attachment ->
            println("${attachment.attachmentName}: ${attachment.tags.size} tags")
            attachment.tags.forEach { tag ->
                println("  📋 ${tag.tagName}")
            }
        }
    }
    is AttachmentResult.Failure -> {
        println("Failed to load attachments: ${result.error.message}")
    }
}
// Get attachment with tags
if let attachment = try await zync.attachments.getAttachment("f47ac10b-58cc-4372-a567-0e02b2c3d479") {
    print("Attachment: \(attachment.attachmentName)")
    print("Tags (\(attachment.tags.count)):")
    for tag in attachment.tags {
        print("  - \(tag.tagName) (\(tag.tagColor ?? "no color"))")
    }
}

// Get all attachments with their tags
let result = try await zync.attachments.getAttachments(
    module: .job,
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    page: 1,
    pageSize: 20
)

switch onEnum(of: result) {
case .success(let success):
    for attachment in success.attachments {
        print("\(attachment.attachmentName): \(attachment.tags.count) tags")
        for tag in attachment.tags {
            print("  📋 \(tag.tagName)")
        }
    }
case .failure(let failure):
    print("Failed to load attachments: \(failure.error.message)")
}

Get Tags for Module

Get all unique tags associated with attachments for a specific module UID. This is useful for understanding what tags are actually being used within a module instance and for building tag filter options:

// Get all tags used in attachments for a specific job
val result = zync.attachments.getTagsByModuleUid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")

// Or search for specific tags - filter by tag name and description
val searchResult = zync.attachments.getTagsByModuleUid("a1b2c3d4-e5f6-7890-abcd-ef1234567890", searchQuery = "urgent")

when (result) {
    is MasterTagsResult.Success -> {
        val tags = result.data
        println("Found ${tags.size} tags used in this module")
        tags.forEach { tag ->
            println("Tag: ${tag.tagName}")
            println("  Color: ${tag.tagColor ?: "no color"}")
            println("  Description: ${tag.tagDescription ?: "no description"}")
        }
        
        // Use tags for filter options
        val tagFilterOptions = tags.map { tag ->
            FilterOption(tag.tagUid, tag.tagName)
        }
    }
    is MasterTagsResult.Failure -> {
        println("Error: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
// Get all tags used in attachments for a specific job
let result = try await zync.attachments.getTagsByModuleUid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")

// Or search for specific tags - filter by tag name and description
let searchResult = try await zync.attachments.getTagsByModuleUid("a1b2c3d4-e5f6-7890-abcd-ef1234567890", searchQuery: "urgent")

switch onEnum(of: result) {
case .success(let success):
    print("Found \(success.data.count) tags used in this module")
    for tag in success.data {
        print("Tag: \(tag.tagName)")
        print("  Color: \(tag.tagColor ?? "no color")")
        print("  Description: \(tag.tagDescription ?? "no description")")
    }
    
    // Use tags for filter options
    let tagFilterOptions = success.data.map { tag in
        FilterOption(tagUid: tag.tagUid, tagName: tag.tagName)
    }
case .failure(let failure):
    print("Error: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Offline Support: Tag operations work offline and sync automatically when connectivity is restored. The SDK queues tag add/remove operations and uploads them during the next sync cycle.

Real-time Updates

Observe Attachment Changes

Monitor attachment changes in real-time using enhanced delta sync events. The SDK provides efficient updates with both updated attachments and deletions in a single event:

// Collect attachment changes with enhanced delta sync
val job = launch {
    zync.attachments.observeAttachmentChanges(
        module = ZyncModule.JOB,
        moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    ).collect { changeEvent ->
        when (changeEvent) {
            is AttachmentChangeEvent.AttachmentsUpdated -> {
                val updatedAttachments = changeEvent.updatedAttachments
                val deletedAttachmentUids = changeEvent.deletedAttachmentUids
                
                // Handle deletions first - remove from current list
                deletedAttachmentUids.forEach { deletedUid ->
                    currentAttachmentList.removeAll { it.attachmentUid == deletedUid }
                    println("Removed deleted attachment: $deletedUid")
                }
                
                // Handle updates and additions
                updatedAttachments.forEach { updatedAttachment ->
                    val existingIndex = currentAttachmentList.indexOfFirst { 
                        it.attachmentUid == updatedAttachment.attachmentUid 
                    }
                    if (existingIndex != -1) {
                        // Update existing attachment
                        currentAttachmentList[existingIndex] = updatedAttachment
                        println("Updated existing attachment: ${updatedAttachment.attachmentName}")
                    } else {
                        // Add new attachment
                        currentAttachmentList.add(updatedAttachment)
                        println("Added new attachment: ${updatedAttachment.attachmentName}")
                    }
                }
                
                // Show notification for changes
                val totalChanges = updatedAttachments.size + deletedAttachmentUids.size
                if (totalChanges > 0) {
                    showUpdateAvailableChip("$totalChanges attachment changes available")
                }
                
                // Refresh UI with updated list
                notifyAttachmentListChanged()
            }
            is AttachmentChangeEvent.AttachmentUploadStatusChanged -> {
                val pendingUpload = changeEvent.pendingUpload
                
                // Find existing attachment in your list by entityUid
                val attachmentUid = pendingUpload.entityUid
                val existingAttachment = currentAttachmentList.find { it.attachmentUid == attachmentUid }
                
                if (existingAttachment != null) {
                    // Update the attachment with new upload status and progress
                    val updatedAttachment = existingAttachment.copy(
                        uploadStatus = pendingUpload.status,
                        // Add progress field if your ZyncAttachment model supports it
                        // uploadProgress = pendingUpload.progress
                    )
                    
                    // Update attachment in list
                    val existingIndex = currentAttachmentList.indexOfFirst { 
                        it.attachmentUid == attachmentUid 
                    }
                    if (existingIndex != -1) {
                        currentAttachmentList[existingIndex] = updatedAttachment
                    }
                    
                    println("Upload status changed for ${existingAttachment.attachmentName}: ${pendingUpload.status}")
                    if (pendingUpload.progress != null) {
                        println("Progress: ${(pendingUpload.progress * 100).toInt()}%")
                    }
                    
                    // Notify UI to refresh this specific attachment item
                    notifyAttachmentItemChanged(attachmentUid)
                }
            }
        }
    }
}

// Cancel when done
job.cancel()
// Observe attachment changes using SKIE's AsyncSequence support
let task = Task {
    for await changeEvent in zync.attachments.observeAttachmentChanges(
        module: .job,
        moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    ) {
        switch changeEvent {
        case .attachmentsUpdated(let updateEvent):
            let updatedAttachments = updateEvent.updatedAttachments
            let deletedAttachmentUids = updateEvent.deletedAttachmentUids
            
            // Handle deletions first - remove from current list
            for deletedUid in deletedAttachmentUids {
                currentAttachmentList.removeAll { $0.attachmentUid == deletedUid }
                print("Removed deleted attachment: \(deletedUid)")
            }
            
            // Handle updates and additions
            for updatedAttachment in updatedAttachments {
                if let existingIndex = currentAttachmentList.firstIndex(where: { 
                    $0.attachmentUid == updatedAttachment.attachmentUid 
                }) {
                    // Update existing attachment
                    currentAttachmentList[existingIndex] = updatedAttachment
                    print("Updated existing attachment: \(updatedAttachment.attachmentName)")
                } else {
                    // Add new attachment
                    currentAttachmentList.append(updatedAttachment)
                    print("Added new attachment: \(updatedAttachment.attachmentName)")
                }
            }
            
            // Show notification for changes
            let totalChanges = updatedAttachments.count + deletedAttachmentUids.count
            if totalChanges > 0 {
                await showUpdateAvailableChip("\(totalChanges) attachment changes available")
            }
            
            // Refresh UI with updated list
            await notifyAttachmentListChanged()
        case .attachmentUploadStatusChanged(let statusEvent):
            let pendingUpload = statusEvent.pendingUpload
            
            // Find existing attachment in your list by entityUid
            let attachmentUid = pendingUpload.entityUid
            if let existingAttachment = currentAttachmentList.first(where: { $0.attachmentUid == attachmentUid }) {
                // Update the attachment with new upload status and progress
                let updatedAttachment = existingAttachment.copy(
                    uploadStatus: pendingUpload.status
                    // Add progress field if your ZyncAttachment model supports it
                    // uploadProgress: pendingUpload.progress
                )
                
                // Update attachment in list
                if let existingIndex = currentAttachmentList.firstIndex(where: { 
                    $0.attachmentUid == attachmentUid 
                }) {
                    currentAttachmentList[existingIndex] = updatedAttachment
                }
                
                print("Upload status changed for \(existingAttachment.attachmentName): \(pendingUpload.status)")
                if let progress = pendingUpload.progress {
                    print("Progress: \(Int(progress * 100))%")
                }
                
                // Notify UI to refresh this specific attachment item
                notifyAttachmentItemChanged(attachmentUid)
            }
        }
    }
}

// Cancel when done
task.cancel()

Deleting Attachments

Delete Attachment

val result = zync.attachments.deleteAttachment("9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f")

when (result) {
    is ZyncResult.Success -> {
        println("Attachment deleted successfully")
    }
    is ZyncResult.Failure -> {
        println("Delete failed: ${result.error.message}")
    }
}
let result = try await zync.attachments.deleteAttachment("9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f")

switch onEnum(of: result) {
case .success:
    print("Attachment deleted successfully")
case .failure(let failure):
    print("Delete failed: \(failure.error.message)")
}

Delete Multiple Attachments

Delete multiple attachments in a single operation for better efficiency:

val attachmentUids = listOf(
    "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    "7c5e1a9b-4d2f-3e8c-9f7g-5h6i7j8k9l0m",
    "3a8d5c2e-6f9b-1c4e-7a8d-2b3c4d5e6f7g"
)

val result = zync.attachments.deleteAttachments(attachmentUids)

when (result) {
    is ZyncResult.Success -> {
        println("Successfully deleted ${attachmentUids.size} attachments")
    }
    is ZyncResult.Failure -> {
        println("Failed to delete attachments: ${result.error.message}")
    }
}
let attachmentUids = [
    "9e4f2c8d-1a3b-4c5d-8e9f-2a3b4c5d6e7f",
    "7c5e1a9b-4d2f-3e8c-9f7g-5h6i7j8k9l0m",
    "3a8d5c2e-6f9b-1c4e-7a8d-2b3c4d5e6f7g"
]

let result = try await zync.attachments.deleteAttachments(attachmentUids: attachmentUids)

switch onEnum(of: result) {
case .success:
    print("Successfully deleted \(attachmentUids.count) attachments")
case .failure(let failure):
    print("Failed to delete attachments: \(failure.error.message)")
}

Error Handling

Common Error Types

result.fold(
    onSuccess = { attachment -> /* Success */ },
    onFailure = { error ->
        when (error) {
            is AttachmentError.FileNotFound -> {
                println("File not found: ${error.message}")
            }
            is AttachmentError.InvalidFilePath -> {
                println("Invalid file path: ${error.message}")
            }
            is AttachmentError.FileCopyError -> {
                println("Failed to copy file: ${error.message}")
            }
            is AttachmentError.InsufficientStorage -> {
                println("Not enough storage: ${error.message}")
            }
            is AttachmentError.AttachmentNotFound -> {
                println("Attachment not found: ${error.message}")
            }
            is AttachmentError.TagNotFound -> {
                println("Tag not found: ${error.message}")
            }
            is AttachmentError.TagAlreadyExists -> {
                println("Tag already exists: ${error.message}")
            }
            is AttachmentError.TagOperationFailed -> {
                println("Tag operation failed: ${error.message}")
            }
            else -> {
                println("Unknown error: ${error.message}")
            }
        }
    }
)
do {
    let result = try await zync.attachments.addAttachment(...)
    // Handle success
} catch AttachmentError.fileNotFound(let message) {
    print("File not found: \(message)")
} catch AttachmentError.invalidFilePath(let message) {
    print("Invalid file path: \(message)")
} catch AttachmentError.fileCopyError(let message) {
    print("Failed to copy file: \(message)")
} catch AttachmentError.insufficientStorage(let message) {
    print("Not enough storage: \(message)")
} catch AttachmentError.attachmentNotFound(let message) {
    print("Attachment not found: \(message)")
} catch AttachmentError.tagNotFound(let message) {
    print("Tag not found: \(message)")
} catch AttachmentError.tagAlreadyExists(let message) {
    print("Tag already exists: \(message)")
} catch AttachmentError.tagOperationFailed(let message) {
    print("Tag operation failed: \(message)")
} catch {
    print("Unknown error: \(error.localizedDescription)")
}

Immediate Upload

The attachment manager provides immediate upload functionality for scenarios where you need to ensure files are uploaded synchronously before proceeding with other operations. This is particularly useful for offline engine integration.

Upload Multiple Files Immediately

Upload a batch of files immediately, bypassing the regular upload queue:

val result = zync.attachments.uploadAttachmentsImmediately(
    filePaths = listOf(
        "file:///storage/emulated/0/Pictures/photo1.jpg",
        "file:///storage/emulated/0/Documents/report.pdf",
        "file:///storage/emulated/0/Pictures/photo2.jpg"
    ),
    module = "job",
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    attachmentType = ZyncAttachmentType.Attachment
)

when (result) {
    is UploadAttachmentsResult.Success -> {
        val uploads = result.successUploads
        println("Successfully uploaded ${uploads.size} files")
        uploads.forEach { upload ->
            println("✓ ${upload.filePath}")
            println("  URL: ${upload.url}")
            println("  UID: ${upload.attachmentUid}")
            println("  Size: ${upload.fileSize} bytes")
        }
    }
    is UploadAttachmentsResult.Failure -> {
        println("Upload failed: ${result.error.message}")
        
        // Check if some files were uploaded before failure
        if (result.successUploads.isNotEmpty()) {
            println("Partial success - ${result.successUploads.size} files uploaded:")
            result.successUploads.forEach { upload ->
                println("✓ ${upload.filePath} -> ${upload.url}")
            }
        }
    }
}
let result = try await zync.attachments.uploadAttachmentsImmediately(
    filePaths: [
        "file:///var/mobile/Documents/photo1.jpg",
        "file:///var/mobile/Documents/report.pdf",
        "file:///var/mobile/Documents/photo2.jpg"
    ],
    module: "job",
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    attachmentType: .attachment
)

switch onEnum(of: result) {
case .success(let success):
    let uploads = success.successUploads
    print("Successfully uploaded \(uploads.count) files")
    for upload in uploads {
        print("✓ \(upload.filePath)")
        print("  URL: \(upload.url)")
        print("  UID: \(upload.attachmentUid)")
        print("  Size: \(upload.fileSize) bytes")
    }
case .failure(let failure):
    print("Upload failed: \(failure.error.message)")
    
    // Check if some files were uploaded before failure
    if !failure.successUploads.isEmpty {
        print("Partial success - \(failure.successUploads.count) files uploaded:")
        for upload in failure.successUploads {
            print("✓ \(upload.filePath) -> \(upload.url)")
        }
    }
}

Upload Single File Immediately

Upload a single file immediately:

val result = zync.attachments.uploadAttachmentImmediately(
    filePath = "file:///storage/emulated/0/Pictures/equipment.jpg",
    module = "job",
    moduleUid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    attachmentType = ZyncAttachmentType.Attachment
)

when (result) {
    is UploadAttachmentResult.Success -> {
        println("File uploaded successfully:")
        println("  URL: ${result.url}")
        println("  UID: ${result.attachmentUid}")
        println("  Path: ${result.filePath}")
        println("  Size: ${result.fileSize} bytes")
    }
    is UploadAttachmentResult.Failure -> {
        println("Upload failed: ${result.error.message}")
        when (result.error) {
            is ZyncError.Network -> {
                println("Check your internet connection")
            }
            is ZyncError.Error -> {
                result.error.code?.let { code ->
                    println("Error code: $code")
                }
            }
        }
    }
}
let result = try await zync.attachments.uploadAttachmentImmediately(
    filePath: "file:///var/mobile/Documents/equipment.jpg",
    module: "job",
    moduleUid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    attachmentType: .attachment
)

switch onEnum(of: result) {
case .success(let success):
    print("File uploaded successfully:")
    print("  URL: \(success.url)")
    print("  UID: \(success.attachmentUid)")
    print("  Path: \(success.filePath)")
    print("  Size: \(success.fileSize) bytes")
case .failure(let failure):
    print("Upload failed: \(failure.error.message)")
    switch onEnum(of: failure.error) {
    case .network:
        print("Check your internet connection")
    case .error(let error):
        print("Error: \(error.message)")
        if let code = error.code {
            print("Error code: \(code)")
        }
    }
}

Performance Note: Immediate uploads bypass the regular upload queue and process files synchronously. This can temporarily pause other upload operations. Use these methods only when synchronous upload completion is required before proceeding with other operations.

Best Practices

1. File URI Validation

Always use proper URI schemes for file paths:

// ✅ Correct - File URI
val fileUri = "file:///storage/emulated/0/Pictures/photo.jpg"

// ✅ Correct - Content URI (Android)
val contentUri = "content://media/picker/0/com.android.providers.media.photopicker/..."

// ❌ Incorrect - Plain path
val plainPath = "/storage/emulated/0/Pictures/photo.jpg"
// ✅ Correct - File URI
let fileUri = "file:///var/mobile/Documents/photo.jpg"

// ❌ Incorrect - Plain path
let plainPath = "/var/mobile/Documents/photo.jpg"

2. Use Real-time Updates

Implement real-time updates for better user experience:

// Keep UI in sync with real-time changes
lifecycleScope.launch {
    zync.attachments.observeAttachmentChanges(ZuperModule.JOB, jobUid)
        .collect { event ->
            updateUI(event)
        }
}
// Keep UI in sync with real-time changes
let cancellable = zync.attachments.observeAttachmentChangesNative(module: .job, moduleUid: jobUid)
    .sink { event in
        updateUI(event)
    }

Supported Modules

Currently, the AttachmentManager supports:

  • ZyncModule.JOB: Job-related attachments

Note: More modules will be supported in future releases.


Copyright © 2025 Zuper Inc. All rights reserved. This software is proprietary and confidential.