Section 6/101 menit

6. Compatibility Layer Pattern

6. Compatibility Layer Pattern

Pattern ini memungkinkan satu API yang sama bekerja di Swift 5 dan Swift 6:

6.1 Sendable Compatibility

swift
// Sources/Compat/Sendable+Compat.swift

// Di Swift 6, Sendable conformance di-enforce compiler.
// Di Swift 5, kita bisa tambahkan secara retroactive sebagai no-op.
#if swift(<6.0)
// Retroactive conformance untuk tipe yang kita tahu thread-safe
// tapi Swift 5 tidak bisa verifikasi
extension URLRequest: @unchecked Sendable {}
extension JSONDecoder: @unchecked Sendable {}
#endif
// Di Swift 6, conformance ini sudah ada di stdlib — tidak perlu deklarasi

6.2 Mutex / Thread-Safety Compatibility

swift
// Sources/Compat/Protected.swift

#if canImport(Synchronization)
import Synchronization

// Alias langsung ke Mutex Swift 6
public struct Protected<Value> {
    private let mutex: Mutex<Value>
    
    public init(_ value: Value) {
        self.mutex = Mutex(value)
    }
    
    public func withLock<R>(_ body: (inout Value) throws -> R) rethrows -> R {
        try mutex.withLock(body)
    }
    
    public func withLockIfAvailable<R>(_ body: (inout Value) throws -> R) rethrows -> R? {
        try mutex.withLockIfAvailable(body)
    }
}

#else
import Foundation

// Fallback: NSLock untuk Swift 5 / OS lama
public final class Protected<Value>: @unchecked Sendable {
    private let lock = NSLock()
    private var value: Value
    
    public init(_ value: Value) {
        self.value = value
    }
    
    public func withLock<R>(_ body: (inout Value) throws -> R) rethrows -> R {
        lock.lock()
        defer { lock.unlock() }
        return try body(&value)
    }
    
    public func withLockIfAvailable<R>(_ body: (inout Value) throws -> R) rethrows -> R? {
        guard lock.try() else { return nil }
        defer { lock.unlock() }
        return try body(&value)
    }
}
#endif

// Penggunaan di kode lain — SAMA, tidak peduli Swift 5 atau 6:
let counter = Protected(0)
counter.withLock { $0 += 1 }

6.3 Observable / ObservableObject Compatibility

swift
// Sources/Compat/Observable+Compat.swift

#if canImport(Observation)
import Observation

// Swift 6 / iOS 17+ / macOS 14+: gunakan @Observable
public typealias ObservableBase = AnyObject  // tidak perlu inherit apapun

// Macro untuk menandai class sebagai observable
@attached(member, names: ...)
@attached(memberAttribute)
@attached(conformance)
public macro Observable() = #externalMacro(module: "ObservationMacros", type: "ObservableMacro")

#else
import Combine

// Swift 5 / iOS 16 / macOS 13: gunakan ObservableObject
public typealias ObservableBase = ObservableObject
#endif
swift
// ViewModel yang kompatibel dengan keduanya:

#if canImport(Observation)
import Observation

@Observable
class UserViewModel {
    var name: String = ""
    var email: String = ""
    var isLoading: Bool = false
}

#else
import Combine

class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var email: String = ""
    @Published var isLoading: Bool = false
}
#endif

6.4 Typed Throws Compatibility

swift
// Sources/Compat/Error+Compat.swift

enum NetworkError: Error {
    case timeout
    case noConnection
    case serverError(Int)
    case invalidResponse
}

// Fungsi dengan typed throws di Swift 6, untyped di Swift 5
// Caller API tetap sama — hanya internals yang berbeda
#if hasFeature(TypedThrows)
func fetchData(from url: URL) async throws(NetworkError) -> Data {
    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let http = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        guard (200...299).contains(http.statusCode) else {
            throw NetworkError.serverError(http.statusCode)
        }
        return data
    } catch is URLError {
        throw NetworkError.timeout
    } catch let error as NetworkError {
        throw error
    } catch {
        throw NetworkError.invalidResponse
    }
}

#else
func fetchData(from url: URL) async throws -> Data {
    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let http = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        guard (200...299).contains(http.statusCode) else {
            throw NetworkError.serverError(http.statusCode)
        }
        return data
    } catch is URLError {
        throw NetworkError.timeout
    }
}
#endif

6.5 Swift Testing / XCTest Compatibility

swift
// Tests/Compat/TestingCompat.swift

#if canImport(Testing)
import Testing

// Gunakan Swift Testing
public typealias SuiteBase = Any  // struct, tidak perlu inherit

// Macro wrappers yang tersedia langsung dari Testing framework

#else
import XCTest

// Fallback ke XCTest
public typealias SuiteBase = XCTestCase
#endif
swift
// Tests/UserServiceTests.swift

#if canImport(Testing)
import Testing

@Suite("UserService Tests")
struct UserServiceTests {
    let service = UserService()
    
    @Test("Fetch user sukses")
    func fetchUserSuccess() async throws {
        let user = try await service.fetchUser(id: "123")
        #expect(user.id == "123")
    }
}

#else
import XCTest

class UserServiceTests: XCTestCase {
    var service: UserService!
    
    override func setUp() {
        super.setUp()
        service = UserService()
    }
    
    func testFetchUserSuccess() async throws {
        let user = try await service.fetchUser(id: "123")
        XCTAssertEqual(user.id, "123")
    }
}
#endif