Attachment Management
Comprehensive guide to using the Zync SDK’s attachment management features.
Table of contents
- Adding Attachments
- Updating Attachments
- Retrieving Attachments
- Tag Management
- Real-time Updates
- Deleting Attachments
- Error Handling
- Immediate Upload
- Best Practices
- 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.