iOS SDK
Native Swift SDK for Abrova Storage on iOS/macOS. Supports async/await (iOS 13+) and completion handler APIs (iOS 11+). Sign users in with an Abrova user token, upload with progress, list, download, delete, public image links and image transforms.
Native Swift SDK for iOS/macOS. Supports async/await (iOS 13+) and completion handler APIs (iOS 11+). No third-party dependencies.
Installation#
Install with Swift Package Manager. In Xcode: File → Add Package Dependencies…, paste https://github.com/Abrova-co/abrova-storage-ios-sdk.git and choose the version. Or in Package.swift:
dependencies: [
.package(url: "https://github.com/Abrova-co/abrova-storage-ios-sdk.git", from: "0.2.1")
]Usage (async/await)#
import AbrovaStorage
// The signed-in user's Abrova user token, see step 3.
// Return nil when no one is signed in.
let storage = AbrovaStorage(apiKey: "ab_live_xxxxxxxxxxxx") {
try await myBackend.fetchAbrovaToken()
}
// Upload a file, with an optional progress callback (main queue)
let file = try await storage.upload(
bucketId: "my-bucket",
path: "photos/avatar.jpg",
data: imageData,
options: UploadOptions(contentType: "image/jpeg"),
progress: { p in progressView.progress = Float(p.fraction) }
)
print("Uploaded: \(file.id)")
// List files
let result = try await storage.listFiles(
bucketId: "my-bucket",
options: ListFilesOptions(prefix: "photos/", limit: 20)
)
for file in result.files {
print(" - \(file.path) (\(file.size) bytes)")
}
// Download
let data = try await storage.download(fileId: file.id)
// Delete (follows the bucket's delete rules)
try await storage.delete(fileId: file.id)The last progress call has bytesSent == totalBytes. totalBytes is the whole request, so it is a little larger than the file.
Sign users in (one token for every Abrova product)#
File requests from apps always need a signed Abrova user token. There is no setting to turn this off. Bucket rules that use {userId} match the user in that token. It is the same token Abrova Sync, Chat, Push and A/B Testing accept.
With Abrova Auth, no backend is needed. Its token is this token:
let auth = AbrovaAuth.shared
let storage = AbrovaStorage(apiKey: "ab_live_xxxxxxxxxxxx", userTokenProvider: auth.tokenProvider)
storage.onAwaitingUserToken = { showSignIn() } // optional; once per wait, background queue
// After sign-in or sign-out
let listener = auth.addAuthStateListener { _ in storage.refreshUserToken() }With your own server, your backend mints the token with POST https://auth.abrova.ir/users/token and your server secret (it never ships in an app), and the provider fetches it, as in step 2. On an existing instance set storage.userTokens.provider = { try await myBackend.fetchAbrovaToken() }; apps that support iOS 11/12 use the callback form:
storage.userTokens.setProvider { completion in
myBackend.fetchAbrovaToken { token in completion(.success(token)) } // .success(nil) = signed out
}
storage.refreshUserToken() // when the user signs in or out
storage.setUserToken(token) // or pass a token you already hold (nil signs out)The SDK sends the token as x-user-token and gets a new one (retrying the request once, uploads included) when it has expired or is refused.
Waiting for a user. With no token (no one signed in), file calls (upload, list, get, download, delete) are not sent and do not fail: they wait, and go on by themselves, in order, once a user signs in. This is by design: show your sign-in screen while they wait (storage.isAwaitingUserToken is true meanwhile). Bucket reads (listBuckets, getBucket, getBucketByName) and getPublicUrl never wait. A refused token with no replacement fails with .userTokenRejected(code:message:).
Don't wait on file calls before sign-in. A file call made while no one is signed in finishes only after sign-in. Don't wait for it in code the user is waiting on (a button handler, a startup check): start it in the background, or after sign-in.
Leave the timeout unset. By default a call waits until sign-in, which is right for background work. Set userTokenWaitTimeout only when a screen waits for the answer and can send the user to sign-in:
let storage = AbrovaStorage(apiKey: "ab_live_xxxxxxxxxxxx",
userTokenWaitTimeout: 30, // seconds; then fails with .userTokenRequired
userTokenProvider: auth.tokenProvider)A call that waited that long fails with AbrovaStorageError.userTokenRequired (token_required). The timeout option is a different setting: it limits each network request (default 30 seconds) and never covers the wait for a user.
With Abrova Auth, turn on the sign-in method you use (for example Anonymous) in Abrova Auth › Sign-in methods. If it is off, sign-in fails with METHOD_DISABLED, no one is signed in, and file calls wait without an error. While developing, log from onAwaitingUserToken (print("Storage: waiting for a user")) so a silent wait is easy to spot.
Upgrading from 0.2.0: userId / setUserId(_:) are removed. Use the token provider.
Showing images#
For a file in a public bucket, getPublicUrl returns a link that needs no key, token or header. With image options it returns a resized or converted image, ready for AsyncImage:
// A 600 px wide WebP, straight into SwiftUI
let url = try await storage.getPublicUrl(fileId: file.id, transforms: ImageTransforms(width: 600, format: "webp"))
AsyncImage(url: URL(string: url)) { image in
image.resizable().scaledToFit()
} placeholder: {
ProgressView()
}
// A public link you stored earlier: the same URL, no request
let thumb = try AbrovaStorage.publicImageUrl(
savedLink,
transforms: ImageTransforms(width: 200, height: 200, fit: "cover", format: "webp")
)- Cached, so use a few fixed sizes. The same options always give the same URL, made once and then served from the cache. One image keeps at most 50 variants; a new one beyond that is refused with
too_many_variants(429). Pick a few sizes (for example 200, 600 and 1200 wide). - Usage. When the month's 500 free transforms are used up and your wallet cannot pay for more, an image URL with options answers
plan_feature_locked(403). The plain public link always works. - Busy.
transform_busy(503) comes with aRetry-Afterheader in seconds: load the image again after that (for example with a new.idon theAsyncImage). - Private buckets have no public link (
getPublicUrlfails with 403). Download the bytes, or loadgetTransformUrlwith your headers:
// Private file: download and show
let data = try await storage.download(fileId: file.id)
let image = UIImage(data: data)
// Or the transform address, which needs the key and user token headers
let transforms = ImageTransforms(width: 200, height: 200, fit: "cover", format: "webp")
try transforms.validate() // invalid_transform before the URL is built
let thumbnailUrl = storage.getTransformUrl(fileId: file.id, transforms: transforms)
var request = URLRequest(url: URL(string: thumbnailUrl)!)
request.setValue("ab_live_xxxxxxxxxxxx", forHTTPHeaderField: "x-api-key")
request.setValue(userToken, forHTTPHeaderField: "x-user-token") // the signed-in user's token
let (thumbData, _) = try await URLSession.shared.data(for: request)Image options: format jpeg (default), png, webp or avif; fit cover, contain, fill, inside or outside; width / height 1 or more (above 4096 sent as 4096); quality 1-100; blur 0-20; sharpen 0-10; rotate degrees. getTransformUrl can't throw, so call transforms.validate() to catch a bad value first. A signed link for a private file comes from your server (getSignedUrl in the Node.js SDK).
Handling errors#
Each AbrovaStorageError the server caused has its HTTP statusCode and, when the server named the reason, its code (constants in StorageErrorCode):
do {
let file = try await storage.upload(bucketId: "photos", path: "clips/a.mov", data: data)
} catch let error as AbrovaStorageError {
if error.isTypeNotAllowed {
showError("This bucket doesn't take this file type")
} else if error.isFileTooLarge {
showError("The file is too big")
} else if error.code == StorageErrorCode.pathTaken {
showError("A file with this name already exists")
} else if error.code == StorageErrorCode.accessDenied {
showError("You can't write here")
} else {
showError(error.localizedDescription)
}
}code is nil when the server gave no reason (a 404, a 429), and statusCode is nil when no answer arrived (.networkError).
| Status | code | When |
|---|---|---|
| 401 | token_required | No user signed in within userTokenWaitTimeout. |
| 401 | token_expired, token_invalid | The user token was refused and the provider had no other one. |
| 403 | access_denied | The bucket's access rules refused the request. |
| 400 | type_not_allowed, file_too_large, executable_blocked | The upload doesn't fit the bucket's file settings. |
| 409 | path_taken | A file already exists at this path. |
| 400 | invalid_path | The path was refused (see below). |
| 400 | invalid_transform | An image option is not allowed. |
| 403 | plan_feature_locked | No transforms left: the free monthly transforms are used up and the wallet cannot pay for more. |
| 429 | too_many_variants | This image already has 50 sizes / formats. |
| 503 | transform_busy | Image processing is busy. Retry after Retry-After seconds. |
Files, buckets and paths#
- A
StorageFilehasid,bucketId,path,fileName,mimeType,size,checksum,metadata,uploadedBy,uploadedAt,createdAtandupdatedAt(checksum,metadata,uploadedByanduploadedAtarenilwhen empty). Dates areDate. - A
Buckethasid,name,description,visibility,allowedMimeTypes(nil= every type),maxFileSize,blockExecutables,createdAtandupdatedAt. listFilesanswersfilesandnextCursor(nilon the last page).- Paths. Pass paths as you write them: spaces,
#,?and non-Latin characters are encoded for you. A.or..part is refused withinvalid_pathbefore anything is sent, and a new file's path can't contain?,#or%. - When you pass no
contentType, the SDK sends the type for the file's extension (photo.HEICasimage/heic,notes.mdastext/markdown).
Completion Handlers (iOS 11+)#
For older iOS versions or callback-based code. The completion form of upload takes the same progress: argument.
storage.listBuckets { result in
switch result {
case .success(let buckets):
print("Found \(buckets.count) buckets")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
storage.upload(
bucketId: "my-bucket",
path: "photo.jpg",
data: imageData,
options: UploadOptions(contentType: "image/jpeg")
) { result in
switch result {
case .success(let file):
print("Uploaded: \(file.id)")
case .failure(let error):
print("Upload failed: \(error)")
}
}