A Powerful, intuitive Swift 6 networking library that makes modern cross-platform development a breeze through composable, protocol-oriented architecture. Blend combines powerful networking and image handling capabilities with deep SwiftUI integration, enabling developers to build reactive, type-safe applications with ease and flexibility.
Blend embraces composability through its protocol hierarchy (AsyncRequestable → AdvancedAsyncRequestable) and SwiftUI-first design with native reactive components, making complex networking patterns feel natural in modern Swift 6 applications.
SwiftUI-First by Design - Native reactive components and view modifiers built specifically for SwiftUI
Protocol-Oriented Composability - Flexible service composition with type-safe hierarchies
Modern Swift Concurrency - Built with async/await and Swift 6 compliance
Cross-Platform - Supports iOS 18+, iPadOS 18+, and macOS 15+
Complete Image Solution - Download, upload, and cache and display images with ease
High Performance - Intelligent LRU caching with configurable limits
Type Safe - Protocol-oriented design with comprehensive error handling
Platform Requirements: iOS 18+ and macOS 15+ provide improved resumable HTTP transfers (URLSession pause/resume and enhanced background reliability)1, HTTP/3 enhancements2, system TLS 1.3 improvements3, and corrected CFNetwork API signatures4.
Support Note: Blend requires iOS 18+/macOS 15+. Platform Feature Matrix:
| Feature | iOS 18+ | macOS 15+ |
|---|---|---|
| Basic Networking | ✅ | ✅ |
| HTTP/3 Support | Improved/where available | Improved/where available |
| TLS 1.3 | Improved | Improved |
| URLSession Pause/Resume | Improved | Improved |
| CFNetwork APIs | Updates in latest SDKs | Updates in latest SDKs |
Add Blend to your project through Xcode:
- File → Add Package Dependency…
- Enter the repository URL:
https://github.com/convenience-init/Blend - Select your version requirements
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/convenience-init/Blend", from: "1.0.0")
]Complete Package.swift example:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [
.iOS(.v18),
.macOS(.v15)
],
dependencies: [
.package(url: "https://github.com/convenience-init/Blend", from: "1.0.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "Blend", package: "Blend")
]
),
.testTarget(
name: "MyAppTests",
dependencies: [
"MyApp",
.product(name: "Blend", package: "Blend")
]
)
]
)Supported Platforms:
- iOS: 18.0+ (includes iPadOS 18.0+)
- macOS: 15.0+
These platforms are both the minimum supported versions and the recommended/tested versions. Blend requires these minimum versions for full Swift 6 concurrency support and modern SwiftUI integration.
import Blend
// Define your endpoint
struct UsersEndpoint: Endpoint {
var scheme: URLScheme = .https
var host: String = "api.example.com"
var path: String = "/users"
var method: RequestMethod = .get
var headers: [String: String]? = ["Accept": "application/json"]
var body: Data? = nil
var queryItems: [URLQueryItem]? = nil
var timeoutDuration: Duration? = .seconds(30)
}
// Create a service that implements AsyncRequestable
class UserService: AsyncRequestable {
typealias ResponseModel = [User] // Documents the primary response type
func getUsers() async throws -> [User] {
return try await sendRequest(to: UsersEndpoint())
}
}For services requiring master-detail patterns, CRUD operations, or complex type hierarchies, use AdvancedAsyncRequestable:
import Blend
// Define endpoints for list and detail views
struct UsersEndpoint: Endpoint {
var scheme: URLScheme = .https
var host: String = "api.example.com"
var path: String = "/users"
var method: RequestMethod = .get
}
struct UserDetailsEndpoint: Endpoint {
let userId: String
var scheme: URLScheme = .https
var host: String = "api.example.com"
var path: String { "/users/\(userId)" }
var method: RequestMethod = .get
}
// Service with both list and detail response types
class UserService: AdvancedAsyncRequestable {
typealias ResponseModel = [UserSummary] // For user lists
typealias SecondaryResponseModel = UserDetails // For user details
// Convenience methods automatically use correct types
func getUsers() async throws -> [UserSummary] {
return try await fetchList(from: UsersEndpoint())
}
func getUserDetails(id: String) async throws -> UserDetails {
return try await fetchDetails(from: UserDetailsEndpoint(userId: id))
}
// Can also use generic sendRequest for custom response types
func createUser(_ input: UserInput) async throws -> UserDetails {
return try await sendRequest(to: CreateUserEndpoint(input: input))
}
}import Blend
class ProductService: AdvancedAsyncRequestable {
typealias ResponseModel = [ProductSummary] // List operations
typealias SecondaryResponseModel = ProductDetails // Detail operations
// List operation - returns summary array
func getProducts() async throws -> [ProductSummary] {
return try await fetchList(from: ProductsEndpoint())
}
// Read operation - returns full details
func getProduct(id: String) async throws -> ProductDetails {
return try await fetchDetails(from: ProductDetailsEndpoint(id: id))
}
// Create operation - returns created item details
func createProduct(_ input: ProductInput) async throws -> ProductDetails {
return try await sendRequest(to: CreateProductEndpoint(input: input))
}
// Update operation - returns updated item details
func updateProduct(id: String, _ input: ProductInput) async throws -> ProductDetails {
return try await sendRequest(to: UpdateProductEndpoint(id: id, input: input))
}
// Delete operation - returns summary (could be just status)
func deleteProduct(id: String) async throws -> ProductSummary {
return try await sendRequest(to: DeleteProductEndpoint(id: id))
}
}import Blend
// Generic CRUD service that works with any AdvancedAsyncRequestable
class GenericCrudService<T: AdvancedAsyncRequestable> {
let service: T
init(service: T) {
self.service = service
}
// Generic list operation
func listItems() async throws -> T.ResponseModel {
// Implementation would use service's fetchList method
fatalError("Implement based on your endpoint pattern")
}
// Generic detail operation
func getItemDetails(id: String) async throws -> T.SecondaryResponseModel {
// Implementation would use service's fetchDetails method
fatalError("Implement based on your endpoint pattern")
}
}
// Usage with type-safe composition
let userCrudService = GenericCrudService(service: UserService())
let productCrudService = GenericCrudService(service: ProductService())
// Both services work with the same generic interface
// but maintain their specific response typesimport Blend
// Base protocol for all API services
protocol ApiService: AdvancedAsyncRequestable {
// Common requirements for all API services
var baseURL: String { get }
var apiKey: String { get }
}
// Specialized service protocols
protocol UserManagementService: ApiService
where ResponseModel: Sequence, ResponseModel.Element == UserSummary {
// User services must use UserSummary for lists
}
protocol ProductManagementService: ApiService
where ResponseModel: Sequence, ResponseModel.Element == ProductSummary {
// Product services must use ProductSummary for lists
}
// Concrete implementations
class ConcreteUserService: UserManagementService {
typealias ResponseModel = [UserSummary]
typealias SecondaryResponseModel = UserDetails
let baseURL = "https://api.example.com"
let apiKey = "your-api-key"
// Implementation...
}
class ConcreteProductService: ProductManagementService {
typealias ResponseModel = [ProductSummary]
typealias SecondaryResponseModel = ProductDetails
let baseURL = "https://api.example.com"
let apiKey = "your-api-key"
// Implementation...
}import Blend
import SwiftUI
// Platform-conditional imports at the top level
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
let imageService = ImageService()
// Fetch image data and convert to SwiftUI Image
do {
let imageData = try await imageService.fetchImageData(from: "https://example.com/image.jpg")
// Platform-standard conversion (platform-specific imports moved to top)
#if canImport(UIKit)
if let uiImage = UIImage(data: imageData) {
let swiftUIImage = Image(uiImage: uiImage)
}
#elseif canImport(AppKit)
if let nsImage = NSImage(data: imageData) {
let swiftUIImage = Image(nsImage: nsImage)
}
#endif
// Blend convenience helper (requires: import Blend)
// Convert data to PlatformImage, then to SwiftUI Image:
// if let platformImage = ImageService.platformImage(from: imageData) {
// let swiftUIImage = Image.from(platformImage: platformImage)
// }
// Use swiftUIImage in your SwiftUI view
// swiftUIImage.resizable().frame(width: 200, height: 200)
} catch {
print("Failed to load image: \(error)")
}
// Check cache first (returns PlatformImage/UIImage/NSImage)
if let cachedImage = imageService.cachedImage(forKey: "https://example.com/image.jpg") {
// Safe conversion using Blend helper (recommended approach)
if let swiftUIImage = Image.from(platformImage: cachedImage) {
// Use swiftUIImage in your SwiftUI view
swiftUIImage.resizable().frame(width: 200, height: 200)
} else {
// Handle conversion failure gracefully
print("Failed to convert cached image to SwiftUI Image")
}
// Alternative: Platform-standard conversion with safe casting
/*
#if canImport(UIKit)
if let uiImage = cachedImage as? UIImage {
let swiftUIImage = Image(uiImage: uiImage)
// Use swiftUIImage in your SwiftUI view
}
#elseif canImport(AppKit)
if let nsImage = cachedImage as? NSImage {
let swiftUIImage = Image(nsImage: nsImage)
// Use swiftUIImage in your SwiftUI view
}
#endif
*/
}import Blend
// Dependency-injected image service
let imageService = ImageService()
// Example PlatformImage (replace with actual image loading)
let uploadURL = URL(string: "https://api.example.com/upload")!
let platformImage: PlatformImage = ... // Load or create your PlatformImage here
// Cross-platform PlatformImage helpers
// Note: NSImage does not natively expose jpegData/pngData - Blend provides these as extensions
// for consistent cross-platform API (iOS/macOS)
// jpegData(compressionQuality: CGFloat) -> Data?
// Creates JPEG data representation with configurable compression quality (0.0 to 1.0)
// Returns nil if image conversion fails
let jpegData = platformImage.jpegData(compressionQuality: 0.8)
// pngData() -> Data?
// Creates PNG data representation with lossless compression
// Returns nil if image conversion fails
let pngData = platformImage.pngData()
// Preferred usage pattern with fallback and error handling
let imageData: Data
if let jpeg = platformImage.jpegData(compressionQuality: 0.8) {
imageData = jpeg
} else if let png = platformImage.pngData() {
imageData = png
} else {
throw NetworkError.imageProcessingFailed
}
let uploadConfig = UploadConfiguration(
fieldName: "photo",
fileName: "profile.jpg",
compressionQuality: 0.8,
additionalFields: ["userId": "123"]
)
// Option 1: Simple async/await upload (no progress tracking)
let multipartResponse = try await imageService.uploadImageMultipart(imageData, to: uploadURL, configuration: uploadConfig)
// Option 2: Upload with progress tracking callback
let base64Response = try await imageService.uploadImageBase64(
imageData,
to: uploadURL,
configuration: uploadConfig
) { progress in
print("Upload progress: \(Int(progress * 100))%")
}
// Upload Method Tradeoffs:
// - Base64 encoding increases payload size by ~33% and can more easily hit request size limits
// - Base64 adds memory and network overhead due to text encoding/decoding
// - Multipart sends binary data directly and is generally preferable for larger files
// - Use Base64 only for small images or when the API requires JSON payloads
// - Multipart uploads are recommended as the default choiceimport SwiftUI
import Blend
struct ImageGalleryView: View {
let imageService: ImageService
var body: some View {
VStack {
BlendImageView(
url: "https://example.com/gallery/1.jpg",
uploadURL: URL(string: "https://api.example.com/upload")!,
uploadType: .multipart,
configuration: UploadConfiguration(),
autoUpload: true, // Automatically upload after loading
imageService: imageService
)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
// Programmatic upload examples
struct UploadView: View {
@State private var uploadResult: String = ""
let imageService: ImageService
var body: some View {
VStack {
BlendImageView(
url: "https://example.com/gallery/1.jpg",
uploadURL: URL(string: "https://api.example.com/upload")!,
uploadType: .multipart,
configuration: UploadConfiguration(),
imageService: imageService
)
.frame(height: 200)
HStack {
// Option 1: Simple upload without progress tracking
Button("Upload (Simple)") {
Task {
do {
let imageView = BlendImageView(/* same parameters */)
let result = try await imageView.uploadImage()
uploadResult = "Upload successful: \(result.count) bytes"
} catch {
uploadResult = "Upload failed: \(error.localizedDescription)"
}
}
}
// Option 2: Upload with progress tracking
Button("Upload (With Progress)") {
Task {
do {
let imageView = BlendImageView(/* same parameters */)
let result = try await imageView.uploadImage { progress in
print("Upload progress: \(Int(progress * 100))%")
}
uploadResult = "Upload successful: \(result.count) bytes"
} catch {
uploadResult = "Upload failed: \(error.localizedDescription)"
}
}
}
}
Text(uploadResult)
}
}
}import SwiftUI
import Blend
struct AdvancedUploadView: View {
@State private var model = AsyncImageModel(imageService: ImageService())
@State private var uploadProgress: Double = 0.0
@State private var isUploading = false
var body: some View {
VStack {
if let image = model.loadedImage {
Image.from(platformImage: image)
.resizable()
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 8))
if isUploading {
ProgressView("Uploading...", value: uploadProgress, total: 1.0)
.progressViewStyle(.linear)
.padding()
}
HStack {
// Option 1: Simple upload without progress tracking
Button("Upload (Simple)") {
Task {
do {
isUploading = true
let result = try await model.uploadImage(
image,
to: URL(string: "https://api.example.com/upload")!,
uploadType: .multipart,
configuration: UploadConfiguration()
)
print("Upload successful: \(result.count) bytes")
} catch {
print("Upload failed: \(error)")
}
isUploading = false
}
}
// Option 2: Upload with progress tracking
Button("Upload (With Progress)") {
Task {
do {
isUploading = true
uploadProgress = 0.0
let result = try await model.uploadImage(
image,
to: URL(string: "https://api.example.com/upload")!,
uploadType: .multipart,
configuration: UploadConfiguration()
) { progress in
uploadProgress = progress
}
print("Upload successful: \(result.count) bytes")
} catch {
print("Upload failed: \(error)")
}
isUploading = false
}
}
}
.disabled(isUploading)
} else if model.isLoading {
ProgressView("Loading image...")
} else {
Button("Load Image") {
Task {
await model.loadImage(from: "https://example.com/image.jpg")
}
}
}
}
.task {
await model.loadImage(from: "https://example.com/image.jpg")
}
}
}