Skip to content

Blend is a powerful, intuitive Swift 6 networking library that makes modern cross-platform development a breeze through composable, protocol-oriented architecture.

License

Notifications You must be signed in to change notification settings

convenience-init/Blend

Repository files navigation

Blend

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 (AsyncRequestableAdvancedAsyncRequestable) and SwiftUI-first design with native reactive components, making complex networking patterns feel natural in modern Swift 6 applications.

Version License: MIT Swift Platforms

Features

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

Installation

Swift Package Manager

Add Blend to your project through Xcode:

  1. File → Add Package Dependency…
  2. Enter the repository URL: https://github.com/convenience-init/Blend
  3. 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")
            ]
        )
    ]
)

Platform Support

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.

Quick Start

Basic Network Request

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())
    }
}

Advanced Networking with Multiple Response Types

For services requiring master-detail patterns, CRUD operations, or complex type hierarchies, use AdvancedAsyncRequestable:

Master-Detail Pattern Example

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))
    }
}

CRUD Operations with Different Response Types

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))
    }
}

Generic Service Composition

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 types

Type-Safe Service Hierarchies

import 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...
}

Image Operations

Download Images

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
    */
}

Upload Images

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 choice

Complete Image Component with Upload

import 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)
        }
    }
}

AsyncImageModel with Progress Tracking

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")
        }
    }
}

Footnotes

  1. URLSession Pause and Resume Documentation

  2. WWDC 2021: Accelerate networking with HTTP/3 and QUIC

  3. Transport Layer Security (TLS) Protocol Versions

  4. CFNetwork Framework Reference

About

Blend is a powerful, intuitive Swift 6 networking library that makes modern cross-platform development a breeze through composable, protocol-oriented architecture.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published