ابرووا
شروع رایگان
☰ فهرست مستندات · iOS
به‌زودی. Abrova Storage به‌زودی فعال می‌شود. این صفحه توضیح می‌دهد چطور کار خواهد کرد؛ هنوز قابل استفاده نیست.
این صفحه هنوز ترجمه نشده — نسخه‌ی انگلیسی نمایش داده می‌شود. English

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.

iOS 0.2.1iOS 11+به‌روزرسانی: ۵ آبان ۱۴۰۵

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:

swift
dependencies: [
    .package(url: "https://github.com/Abrova-co/abrova-storage-ios-sdk.git", from: "0.2.1")
]

Usage (async/await)#

swift
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:

swift
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:

swift
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:

swift
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:

swift
// 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 a Retry-After header in seconds: load the image again after that (for example with a new .id on the AsyncImage).
  • Private buckets have no public link (getPublicUrl fails with 403). Download the bytes, or load getTransformUrl with your headers:
swift
// 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):

swift
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).

StatuscodeWhen
401token_requiredNo user signed in within userTokenWaitTimeout.
401token_expired, token_invalidThe user token was refused and the provider had no other one.
403access_deniedThe bucket's access rules refused the request.
400type_not_allowed, file_too_large, executable_blockedThe upload doesn't fit the bucket's file settings.
409path_takenA file already exists at this path.
400invalid_pathThe path was refused (see below).
400invalid_transformAn image option is not allowed.
403plan_feature_lockedNo transforms left: the free monthly transforms are used up and the wallet cannot pay for more.
429too_many_variantsThis image already has 50 sizes / formats.
503transform_busyImage processing is busy. Retry after Retry-After seconds.

Files, buckets and paths#

  • A StorageFile has id, bucketId, path, fileName, mimeType, size, checksum, metadata, uploadedBy, uploadedAt, createdAt and updatedAt (checksum, metadata, uploadedBy and uploadedAt are nil when empty). Dates are Date.
  • A Bucket has id, name, description, visibility, allowedMimeTypes (nil = every type), maxFileSize, blockExecutables, createdAt and updatedAt.
  • listFiles answers files and nextCursor (nil on the last page).
  • Paths. Pass paths as you write them: spaces, #, ? and non-Latin characters are encoded for you. A . or .. part is refused with invalid_path before 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.HEIC as image/heic, notes.md as text/markdown).

Completion Handlers (iOS 11+)#

For older iOS versions or callback-based code. The completion form of upload takes the same progress: argument.

swift
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)")
    }
}
این صفحه مفید بود؟
گزارش مشکل