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