Εισαγωγή: Γιατί το Swift Concurrency Αλλάζει τα Πάντα
Ας είμαστε ειλικρινείς — ο ταυτόχρονος προγραμματισμός ήταν πάντα ένας εφιάλτης στην ανάπτυξη εφαρμογών iOS. Από τα NSThread και τα GCD queues μέχρι τα OperationQueues, κάθε γενιά εργαλείων μας έδινε βελτιώσεις — αλλά και νέους πονοκεφάλους. Callbacks μέσα σε callbacks, race conditions που εμφανίζονταν μόνο σε production (φυσικά, πότε αλλιώς;), και memory leaks από κυκλικές αναφορές σε completion handlers.
Το Swift Concurrency αλλάζει τα δεδομένα. Εισήχθη αρχικά στο Swift 5.5 και έχει εξελιχθεί σημαντικά μέχρι το Swift 6.2, αντιμετωπίζοντας αυτά τα προβλήματα με ένα ριζικά διαφορετικό μοντέλο. Αντί να σκέφτεστε σε queues και threads, σκέφτεστε σε tasks, actors και structured concurrency — ένα μοντέλο που ο compiler μπορεί να ελέγξει στατικά για data races.
Λοιπόν, ας βουτήξουμε. Σε αυτόν τον οδηγό θα εξερευνήσουμε κάθε πτυχή του Swift Concurrency: από τα βασικά του async/await μέχρι τους actors, τα task groups, και τις σημαντικές αλλαγές που έφερε το Swift 6.2 με το «Approachable Concurrency». Κάθε ενότητα περιλαμβάνει πρακτικά παραδείγματα κώδικα που μπορείτε να δοκιμάσετε αμέσως.
Async/Await: Η Βάση του Σύγχρονου Ασύγχρονου Κώδικα
Το async/await αντικαθιστά τα completion handlers με κώδικα που διαβάζεται σειριακά, σαν να ήταν σύγχρονος. Και δεν είναι απλώς «syntactic sugar» — αλλάζει θεμελιωδώς τον τρόπο που η Swift διαχειρίζεται ασύγχρονες λειτουργίες.
Πριν: Completion Handlers
func fetchUserProfile(id: String, completion: @escaping (Result<User, Error>) -> Void) {
networkClient.request("/users/\(id)") { result in
switch result {
case .success(let data):
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
Μετά: Async/Await
func fetchUserProfile(id: String) async throws -> User {
let data = try await networkClient.request("/users/\(id)")
let user = try JSONDecoder().decode(User.self, from: data)
return user
}
Η διαφορά είναι δραματική, σωστά; Ο κώδικας με async/await είναι γραμμικός, εύκολος στην ανάγνωση, και — πολύ σημαντικό — εύκολος στη διαχείριση σφαλμάτων με try/catch. Δεν χρειάζεται πλέον να θυμάστε αν καλέσατε το completion handler σε όλα τα paths.
Πώς Δουλεύει Εσωτερικά
Όταν μια συνάρτηση async φτάσει σε ένα await, δεν μπλοκάρει το thread. Αντ' αυτού, «αναστέλλεται» (suspends) και απελευθερώνει το thread για άλλη εργασία. Όταν η ασύγχρονη λειτουργία ολοκληρωθεί, η εκτέλεση συνεχίζεται — αλλά προσοχή: δεν είναι εγγυημένο ότι θα γίνει στο ίδιο thread.
func loadDashboard() async throws -> Dashboard {
// Σημείο 1: Τρέχει σε κάποιο thread
let user = try await fetchUserProfile(id: currentUserId)
// Σημείο 2: Μπορεί να τρέχει σε διαφορετικό thread
let stats = try await fetchUserStats(for: user)
// Σημείο 3: Και πάλι, πιθανώς διαφορετικό thread
return Dashboard(user: user, stats: stats)
}
Αυτό είναι κρίσιμο: μεταξύ δύο await, το thread μπορεί να αλλάξει. Γι' αυτό δεν πρέπει ποτέ να βασίζεστε σε thread-local storage μέσα σε async context.
Structured Concurrency: Tasks και Task Groups
Ένα από τα πιο σημαντικά concepts του Swift Concurrency είναι η structured concurrency (δομημένη ταυτοχρονικότητα). Η βασική αρχή είναι απλή: κάθε child task δεν μπορεί να ξεπεράσει σε διάρκεια ζωής τη γονική εργασία του. Αυτό εξαλείφει ολόκληρη κατηγορία bugs με resource leaks.
Async Let: Παράλληλη Εκτέλεση για Σταθερό Αριθμό Εργασιών
Όταν ξέρετε εκ των προτέρων πόσες εργασίες θέλετε να τρέξετε παράλληλα, το async let είναι η πιο απλή (και κομψή) επιλογή:
func loadProfileScreen(userId: String) async throws -> ProfileScreen {
async let user = fetchUserProfile(id: userId)
async let photos = fetchUserPhotos(userId: userId)
async let followers = fetchFollowerCount(userId: userId)
// Και τα τρία requests τρέχουν παράλληλα
// Εδώ περιμένουμε τα αποτελέσματα
return try await ProfileScreen(
user: user,
photos: photos,
followerCount: followers
)
}
Χωρίς το async let, αυτές οι τρεις κλήσεις θα εκτελούνταν σειριακά — η μία μετά την άλλη. Με async let, ξεκινούν ταυτόχρονα και τα αποτελέσματα συλλέγονται στο σημείο χρήσης. Σκεφτείτε πόση ώρα εξοικονομείτε σε μια οθόνη με πολλά API calls.
Task Groups: Δυναμικός Αριθμός Παράλληλων Εργασιών
Τι γίνεται όμως αν δεν ξέρετε πόσες εργασίες θα τρέξετε; Για παράδειγμα, αν θέλετε να κατεβάσετε πολλές εικόνες — ίσως 5, ίσως 500. Εκεί μπαίνουν τα task groups:
func downloadAllImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
for (index, url) in urls.enumerated() {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return (index, image)
}
}
var images: [(Int, UIImage)] = []
for try await result in group {
images.append(result)
}
// Ταξινόμηση κατά αρχική σειρά
return images.sorted(by: { $0.0 < $1.0 }).map(\.1)
}
}
Ένα κρίσιμο σημείο εδώ: αν κάποιο task πετάξει σφάλμα μέσα στο withThrowingTaskGroup, όλα τα υπόλοιπα child tasks ακυρώνονται αυτόματα. Αυτή η αυτόματη ακύρωση είναι ειλικρινά ένα από τα πιο υποτιμημένα πλεονεκτήματα της structured concurrency.
Έλεγχος Ταυτοχρονικότητας σε Task Groups
Αν έχετε χιλιάδες URLs, σίγουρα δεν θέλετε να τα κατεβάσετε όλα ταυτόχρονα — θα εξαντλήσετε τη μνήμη πριν προλάβετε να πείτε «crash». Η λύση;
func downloadImagesWithLimit(urls: [URL], maxConcurrent: Int = 5) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
var iterator = urls.makeIterator()
// Ξεκινάμε τα πρώτα N tasks
for _ in 0..<min(maxConcurrent, urls.count) {
if let url = iterator.next() {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
}
// Κάθε φορά που ένα task ολοκληρώνεται, προσθέτουμε νέο
for try await image in group {
images.append(image)
if let url = iterator.next() {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
}
return images
}
}
Αυτό το pattern — που χρησιμοποιεί τη σειριακή ανάγνωση αποτελεσμάτων μέσω for try await — εγγυάται ότι ποτέ δεν θα τρέχουν περισσότερα από maxConcurrent tasks ταυτόχρονα. Κομψό, ε;
Actors: Ασφαλής Κοινή Κατάσταση
Τα race conditions. Αν είστε developer αρκετό καιρό, ξέρετε ότι είναι από τα πιο ύπουλα bugs — εμφανίζονται σπάνια, είναι σχεδόν αδύνατο να αναπαραχθούν, και μπορούν να προκαλέσουν crashes ή data corruption τις χειρότερες στιγμές. Οι actors της Swift λύνουν αυτό το πρόβλημα στο επίπεδο του compiler.
Τι Είναι ένας Actor
Ένας actor μοιάζει πολύ με class, αλλά με μια κρίσιμη διαφορά: εγγυάται ότι μόνο ένα task μπορεί να προσπελάσει τα mutable properties του κάθε στιγμή. Το ωραίο; Αυτό γίνεται αυτόματα — δεν χρειάζεται να διαχειρίζεστε locks ή semaphores χειροκίνητα.
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var inProgressRequests: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
// Αν υπάρχει στο cache, επέστρεψέ το
if let cached = cache[url] {
return cached
}
// Αν υπάρχει ήδη request σε εξέλιξη, περίμενε αυτό
if let existingTask = inProgressRequests[url] {
return try await existingTask.value
}
// Δημιούργησε νέο request
let task = Task {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
inProgressRequests[url] = task
do {
let image = try await task.value
cache[url] = image
inProgressRequests[url] = nil
return image
} catch {
inProgressRequests[url] = nil
throw error
}
}
func clearCache() {
cache.removeAll()
}
}
Σε αυτό το παράδειγμα, ο ImageCache actor εγγυάται thread-safety χωρίς κανένα lock. Το dictionary cache δεν μπορεί ποτέ να προσπελαστεί ταυτόχρονα από δύο tasks. Αν το σκεφτείτε, αυτό μόνο του εξαλείφει τεράστιο αριθμό potential bugs.
Actor Reentrancy: Μια Σημαντική Λεπτομέρεια
Εδώ υπάρχει μια παγίδα που πολλοί developers παραβλέπουν: οι actors δεν εμποδίζουν reentrancy. Κάθε φορά που ένας actor φτάσει σε ένα await, μπορεί να εξυπηρετήσει άλλο request πριν ολοκληρωθεί το πρώτο:
actor Counter {
var value = 0
func increment() async {
let current = value // Διαβάζουμε value = 0
await someAsyncWork() // Εδώ ο actor μπορεί να εξυπηρετήσει άλλο call
value = current + 1 // Μπορεί να πατήσουμε πάνω σε αλλαγές!
}
}
// Σωστή υλοποίηση
actor SafeCounter {
var value = 0
func increment() async {
await someAsyncWork()
value += 1 // Διαβάζουμε και γράφουμε value σε ένα βήμα
}
}
Ο κανόνας είναι απλός: μην υποθέτετε ότι η κατάσταση ενός actor δεν έχει αλλάξει μετά από ένα await. Πάντα ελέγξτε ξανά.
@MainActor: Ο Actor του UI
Ο @MainActor είναι ένας global actor που εγγυάται ότι ο κώδικας τρέχει στο main thread — ακριβώς εκεί που πρέπει να γίνονται οι ενημερώσεις του UI:
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
private let imageCache = ImageCache()
func loadProfile(userId: String) async {
isLoading = true
errorMessage = nil
do {
let user = try await fetchUserProfile(id: userId)
self.user = user // Ασφαλής ενημέρωση UI — τρέχουμε στο main thread
} catch {
errorMessage = "Αποτυχία φόρτωσης: \(error.localizedDescription)"
}
isLoading = false
}
}
Παρατηρήστε ότι δεν χρειάζεται DispatchQueue.main.async πουθενά. Τέρμα αυτό. Ο @MainActor εγγυάται ότι όλες οι properties και μέθοδοι της κλάσης εκτελούνται στο main thread.
Sendable: Ασφαλής Μεταφορά Δεδομένων
Όταν στέλνετε δεδομένα μεταξύ actors ή tasks, η Swift πρέπει να βεβαιωθεί ότι αυτά τα δεδομένα μπορούν να μεταφερθούν ασφαλώς. Αυτός είναι ο ρόλος του πρωτοκόλλου Sendable.
// Τα structs με Sendable properties είναι αυτόματα Sendable
struct UserProfile: Sendable {
let id: String
let name: String
let email: String
}
// Τα classes χρειάζονται ειδική προσοχή
final class AppConfiguration: Sendable {
let apiBaseURL: URL // let = immutable = safe
let maxRetries: Int // let = immutable = safe
// var mutableState: String // Αυτό θα ήταν compile error!
}
// Actors είναι αυτόματα Sendable
actor DataStore: Sendable {
var items: [String] = []
}
Η γενική συμβουλή μου; Χρησιμοποιήστε structs όσο περισσότερο μπορείτε. Τα value types είναι εγγενώς ασφαλή για μεταφορά μεταξύ isolation contexts, γιατί κάθε φορά δημιουργείται αντίγραφο.
Ακύρωση Tasks: Cooperative Cancellation
Η ακύρωση στο Swift Concurrency λειτουργεί με τρόπο cooperative — δηλαδή το σύστημα σηματοδοτεί ένα task για ακύρωση, αλλά είναι δική σας ευθύνη να ελέγξετε και να αντιδράσετε κατάλληλα:
func processLargeDataset(items: [DataItem]) async throws -> [ProcessedItem] {
var results: [ProcessedItem] = []
for item in items {
// Ελέγχουμε αν το task έχει ακυρωθεί
try Task.checkCancellation()
let processed = await process(item)
results.append(processed)
}
return results
}
// Εναλλακτικά, για πιο ελεγχόμενη ακύρωση:
func processWithGracefulCancellation(items: [DataItem]) async -> [ProcessedItem] {
var results: [ProcessedItem] = []
for item in items {
if Task.isCancelled {
// Αντί να πετάξουμε σφάλμα, επιστρέφουμε ό,τι έχουμε
break
}
let processed = await process(item)
results.append(processed)
}
return results
}
Η Task.checkCancellation() πετάει CancellationError αν το task έχει ακυρωθεί, ενώ η Task.isCancelled σας δίνει τη δυνατότητα να χειριστείτε την ακύρωση πιο ομαλά. Ποια θα επιλέξετε εξαρτάται από το αν θέλετε να κρατήσετε τα μερικά αποτελέσματα ή όχι.
Τι Αλλάζει στο Swift 6.2: Approachable Concurrency
Αν δουλέψατε με Swift Concurrency σε παλαιότερες εκδόσεις, πιθανότατα έχετε βιώσει ένα πολύ συνηθισμένο frustration: «Γιατί ο κώδικάς μου τρέχει σε background thread ενώ δεν το ζήτησα;»
Ομολογώ ότι κι εγώ έχω χάσει ώρες debugging ακριβώς αυτό το πρόβλημα.
Το Swift 6.2 αντιμετωπίζει αυτό και πολλά άλλα ζητήματα με μια φιλοσοφία που ονομάζεται Approachable Concurrency. Η βασική ιδέα: ο κώδικάς σας τρέχει single-threaded εκτός αν ρητά ζητήσετε κάτι διαφορετικό.
1. nonisolated(nonsending) ως Προεπιλογή (SE-0461)
Αυτή είναι ίσως η πιο σημαντική αλλαγή. Στο Swift 6.1 και παλαιότερα, υπήρχε μια ενοχλητική ασυνέπεια:
// Swift 6.1 — ΑΣΥΝΕΠΗΣ ΣΥΜΠΕΡΙΦΟΡΑ
class MyService {
// Αυτή η συνάρτηση τρέχει στον actor του καλούντα
nonisolated func syncWork() { }
// Αυτή η συνάρτηση πηγαίνει σε BACKGROUND thread!
nonisolated func asyncWork() async { }
}
// Swift 6.2 — ΣΥΝΕΠΗΣ ΣΥΜΠΕΡΙΦΟΡΑ
class MyService {
// Τρέχει στον actor του καλούντα
nonisolated func syncWork() { }
// Τώρα τρέχει ΕΠΙΣΗΣ στον actor του καλούντα
nonisolated func asyncWork() async { }
}
Στο Swift 6.2, οι nonisolated async συναρτήσεις τρέχουν πλέον στον actor του καλούντα, ακριβώς όπως οι σύγχρονες εκδόσεις τους. Αν καλέσετε μια τέτοια συνάρτηση από τον @MainActor, θα τρέξει στο main thread. Επιτέλους, συνέπεια!
2. @concurrent: Ρητή Μετάβαση σε Background
Τι γίνεται όμως αν θέλετε πραγματικά μια συνάρτηση να τρέξει σε background thread; Εκεί μπαίνει το @concurrent:
@MainActor
class DataProcessor {
// Αυτή τρέχει στο main thread (nonisolated nonsending by default)
func quickTransform(data: Data) async -> String {
// Ελαφριά εργασία — OK στο main thread
return String(data: data, encoding: .utf8) ?? ""
}
// Αυτή τρέχει ΡΗΤΑ σε background thread
@concurrent
nonisolated func heavyProcessing(data: Data) async throws -> ProcessedResult {
// Βαριά CPU εργασία — δεν θέλουμε να μπλοκάρει το UI
let result = try performExpensiveComputation(data)
return result
}
}
Το @concurrent είναι ο ρητός τρόπος να πείτε «θέλω αυτή τη συνάρτηση σε background». Πρέπει πάντα να συνοδεύεται από nonisolated — κάτι λογικό αν το σκεφτείτε.
3. Default MainActor Isolation (SE-0466)
Αυτό είναι game-changer. Νέα projects στο Xcode 26 ενεργοποιούν αυτόματα το defaultIsolation(MainActor.self). Κάθε τύπος στο module σας λαμβάνει αυτόματα ένα implicit @MainActor:
// Χωρίς defaultIsolation — πρέπει να βάλετε @MainActor παντού
@MainActor
class SettingsViewModel: ObservableObject { ... }
@MainActor
class ProfileViewModel: ObservableObject { ... }
@MainActor
struct ContentView: View { ... }
// Με defaultIsolation(MainActor.self) — όλα είναι @MainActor αυτόματα
class SettingsViewModel: ObservableObject { ... } // αυτόματα @MainActor
class ProfileViewModel: ObservableObject { ... } // αυτόματα @MainActor
struct ContentView: View { ... } // αυτόματα @MainActor
Η φιλοσοφία εδώ είναι η progressive disclosure: δεν χρειάζεται να ξέρετε για concurrency μέχρι να το χρειαστείτε πραγματικά. Ο κώδικάς σας τρέχει single-threaded εκτός αν εσείς αποφασίσετε διαφορετικά. Και ειλικρινά, για τις περισσότερες εφαρμογές αυτό είναι ακριβώς αυτό που θέλετε.
4. Inferred Isolated Conformances (SE-0470)
Στο Swift 6.1, αν είχατε μια @MainActor κλάση που ήθελε να κάνει conform σε Equatable, ο compiler παραπονιόταν. Η == operator θα μπορούσε να κληθεί από οποιοδήποτε context, κι αυτό δημιουργούσε πρόβλημα:
// Swift 6.2 — Isolated Conformances
@MainActor
class UserSettings: Equatable {
var theme: Theme
var fontSize: Int
// Αυτό λειτουργεί τώρα χωρίς πρόβλημα!
// Η conformance είναι isolated στον MainActor
static func == (lhs: UserSettings, rhs: UserSettings) -> Bool {
lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
}
}
Πλέον, ο compiler αναγνωρίζει ότι η conformance είναι isolated — λειτουργεί μόνο μέσα από τον αντίστοιχο actor context. Τέρμα τα θορυβώδη warnings που σας έκαναν να αμφιβάλλετε αν κάνατε κάτι λάθος.
5. Ενεργοποίηση στο Project σας
Για Xcode projects, ψάξτε το «Approachable Concurrency» στα Build Settings. Για Swift packages:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyApp",
targets: [
.executableTarget(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InferSendableFromCaptures"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("GlobalActorIsolatedTypesUsability")
]
)
]
)
Πρακτικό Παράδειγμα: Ολοκληρωμένη Εφαρμογή με SwiftUI
Αρκετά με τη θεωρία — ας δούμε ένα πλήρες παράδειγμα που συνδυάζει όλα τα παραπάνω σε μια πραγματική εφαρμογή. Θα φτιάξουμε μια οθόνη που φορτώνει δεδομένα από πολλαπλά endpoints παράλληλα:
// MARK: - Models
struct Article: Codable, Sendable, Identifiable {
let id: Int
let title: String
let body: String
let imageURL: URL?
}
struct Category: Codable, Sendable, Identifiable {
let id: Int
let name: String
}
// MARK: - Network Layer
actor ArticleService {
private let session = URLSession.shared
private let baseURL = URL(string: "https://api.example.com")!
func fetchArticles(category: Int) async throws -> [Article] {
let url = baseURL.appending(path: "/articles")
.appending(queryItems: [.init(name: "category", value: "\(category)")])
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode([Article].self, from: data)
}
func fetchCategories() async throws -> [Category] {
let url = baseURL.appending(path: "/categories")
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode([Category].self, from: data)
}
func fetchArticle(id: Int) async throws -> Article {
let url = baseURL.appending(path: "/articles/\(id)")
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(Article.self, from: data)
}
}
// MARK: - ViewModel
@MainActor
@Observable
class ArticleListViewModel {
var articles: [Article] = []
var categories: [Category] = []
var isLoading = false
var errorMessage: String?
private let service = ArticleService()
func loadInitialData() async {
isLoading = true
errorMessage = nil
do {
// Παράλληλη φόρτωση άρθρων και κατηγοριών
async let fetchedArticles = service.fetchArticles(category: 0)
async let fetchedCategories = service.fetchCategories()
let (articlesResult, categoriesResult) = try await (fetchedArticles, fetchedCategories)
articles = articlesResult
categories = categoriesResult
} catch {
errorMessage = "Σφάλμα φόρτωσης: \(error.localizedDescription)"
}
isLoading = false
}
func loadArticlesForAllCategories() async {
isLoading = true
do {
let cats = try await service.fetchCategories()
categories = cats
// Φόρτωση άρθρων για κάθε κατηγορία παράλληλα
articles = try await withThrowingTaskGroup(of: [Article].self) { group in
for category in cats {
group.addTask { [service] in
try await service.fetchArticles(category: category.id)
}
}
var allArticles: [Article] = []
for try await categoryArticles in group {
allArticles.append(contentsOf: categoryArticles)
}
return allArticles
}
} catch {
errorMessage = "Σφάλμα: \(error.localizedDescription)"
}
isLoading = false
}
}
// MARK: - SwiftUI View
struct ArticleListView: View {
@State private var viewModel = ArticleListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Φόρτωση άρθρων...")
} else if let error = viewModel.errorMessage {
ContentUnavailableView(
"Σφάλμα",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
} else {
List(viewModel.articles) { article in
ArticleRowView(article: article)
}
}
}
.navigationTitle("Άρθρα")
.task {
await viewModel.loadInitialData()
}
.refreshable {
await viewModel.loadInitialData()
}
}
}
}
struct ArticleRowView: View {
let article: Article
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(article.title)
.font(.headline)
Text(article.body)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
.padding(.vertical, 4)
}
}
Βέλτιστες Πρακτικές και Συχνά Λάθη
Μετά από αρκετό καιρό δουλειάς με το Swift Concurrency, υπάρχουν κάποια patterns που αξίζει να κρατήσετε σαν σημειώσεις:
1. Μην Χρησιμοποιείτε Detached Tasks Χωρίς Λόγο
// Αποφύγετε αυτό
Task.detached {
await self.doWork()
}
// Προτιμήστε αυτό
Task {
await self.doWork()
}
Τα detached tasks δεν κληρονομούν τον actor ούτε την προτεραιότητα του γονικού task. Χρησιμοποιήστε τα μόνο όταν πραγματικά χρειάζεστε αυτή τη συμπεριφορά — π.χ. background logging ή analytics.
2. Μην Αναμιγνύετε GCD με Swift Concurrency
// Μη συνδυάζετε τα δύο
DispatchQueue.global().async {
Task {
await self.doAsyncWork()
}
}
// Χρησιμοποιήστε μόνο Swift Concurrency
Task {
await self.doAsyncWork()
}
Σοβαρά, μην τα μπλέκετε. Δημιουργεί πολύ δυσνόητα bugs.
3. Χρησιμοποιήστε .task Αντί για Task σε Views
// Memory leak risk
struct MyView: View {
var body: some View {
Text("Hello")
.onAppear {
Task {
await loadData()
}
}
}
}
// Αυτόματη ακύρωση όταν το view αφαιρεθεί
struct MyView: View {
var body: some View {
Text("Hello")
.task {
await loadData()
}
}
}
Ο modifier .task ακυρώνει αυτόματα το task όταν το view φύγει από την οθόνη — κάτι που δεν κάνει αυτόματα το Task { } μέσα σε .onAppear. Αυτή η μικρή λεπτομέρεια μπορεί να σας γλιτώσει από αρκετά memory leaks.
4. Χρησιμοποιήστε AsyncStream για Event-Based APIs
Αν χρειάζεται να μετατρέψετε ένα delegate-based API σε async/await, το AsyncStream είναι ο δρόμος:
class LocationManager {
func locationUpdates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
let delegate = LocationDelegate(
onUpdate: { location in
continuation.yield(location)
},
onFinish: {
continuation.finish()
}
)
continuation.onTermination = { _ in
delegate.stopUpdating()
}
delegate.startUpdating()
}
}
}
// Χρήση
for await location in locationManager.locationUpdates() {
print("Νέα τοποθεσία: \(location.coordinate)")
}
5. Μην Αγνοείτε τα Warnings του Compiler
Το Swift 6 και 6.2 παράγουν πολλά concurrency warnings. Μην τα αγνοείτε — κάθε warning υποδεικνύει ένα πιθανό data race. Αντιμετωπίστε τα ένα-ένα, ξεκινώντας από τα modules που αλλάζουν λιγότερο συχνά. Ξέρω ότι μπαίνει ο πειρασμός να τα «σβήσετε», αλλά μην το κάνετε.
Μετάβαση από GCD σε Swift Concurrency
Αν έχετε υπάρχον project γεμάτο GCD, η μετάβαση μπορεί να γίνει σταδιακά. Δεν χρειάζεται να ξαναγράψετε τα πάντα μονομιάς — στην πραγματικότητα, αυτό θα ήταν κακή ιδέα. Ακολουθεί ένας πρακτικός οδηγός βήμα-βήμα:
Βήμα 1: Ξεκινήστε από το Επίπεδο Δικτύου
// Πριν: GCD + Completion Handler
func fetchData(url: URL, completion: @escaping (Data?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
completion(data)
}.resume()
}
// Μετά: Async/Await
func fetchData(url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
Βήμα 2: Μετατρέψτε τα ViewModels
// Πριν
class ViewModel: ObservableObject {
@Published var items: [Item] = []
func load() {
DispatchQueue.global().async { [weak self] in
let items = self?.fetchItems() ?? []
DispatchQueue.main.async {
self?.items = items
}
}
}
}
// Μετά
@MainActor
@Observable
class ViewModel {
var items: [Item] = []
func load() async {
items = await fetchItems()
}
}
Βήμα 3: Αντικαταστήστε τα Locks με Actors
// Πριν
class ThreadSafeCache {
private let lock = NSLock()
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Any) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
// Μετά
actor Cache {
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
storage[key]
}
func set(_ key: String, value: Any) {
storage[key] = value
}
}
Αντιμετώπιση Κοινών Σφαλμάτων
Ας δούμε τα πιο συχνά σφάλματα που θα συναντήσετε (γιατί θα τα συναντήσετε, σίγουρα) και πώς να τα λύσετε:
«Non-sendable type captured in @Sendable closure»
// Πρόβλημα
class MyClass {
var data: [String] = []
func process() async {
Task {
data.append("new") // Non-sendable!
}
}
}
// Λύση 1: Χρήση actor
actor MyActor {
var data: [String] = []
func process() {
data.append("new")
}
}
// Λύση 2: Χρήση @MainActor
@MainActor
class MyClass {
var data: [String] = []
func process() {
Task {
data.append("new") // OK — ίδιος actor
}
}
}
«Actor-isolated property cannot be referenced from non-isolated context»
// Πρόβλημα
actor DataStore {
var items: [String] = []
}
let store = DataStore()
print(store.items) // Σφάλμα!
// Λύση: Χρήση await
let items = await store.items
print(items)
Σύνοψη και Επόμενα Βήματα
Το Swift Concurrency δεν είναι απλώς ένα νέο API — είναι ένα ολοκληρωμένο σύστημα που λύνει πραγματικά, καθημερινά προβλήματα:
- Το async/await εξαλείφει τα callback hells και κάνει τον ασύγχρονο κώδικα αναγνώσιμο
- Η structured concurrency με task groups και async let εξασφαλίζει ότι κανένα task δεν ξεχνιέται
- Οι actors εξαλείφουν τα data races στο επίπεδο του compiler
- Το Sendable εγγυάται ασφαλή μεταφορά δεδομένων μεταξύ isolation domains
- Το Swift 6.2 με το Approachable Concurrency κάνει όλα τα παραπάνω πιο προσιτά από ποτέ
Αν ξεκινάτε νέο project, η σύστασή μου είναι ξεκάθαρη: ενεργοποιήστε αμέσως τα features του Swift 6.2 — defaultIsolation(MainActor.self) και NonisolatedNonsendingByDefault. Αν έχετε υπάρχον project, ξεκινήστε τη μετάβαση σταδιακά, ένα feature τη φορά. Μην βιάζεστε.
Ο ταυτόχρονος προγραμματισμός δεν χρειάζεται πλέον να μας τρομάζει. Με τα σωστά εργαλεία και patterns, μπορούμε να γράφουμε ασφαλή, αποδοτικό και αναγνώσιμο κώδικα — και η Swift 6.2 κάνει αυτό ευκολότερο από ποτέ.