Concurența în Swift: Ghid Complet async/await, Actors, TaskGroup și Sendable

Învață concurența modernă în Swift: de la async/await și TaskGroup la Actors, Sendable și regulile stricte din Swift 6. Ghid practic cu exemple funcționale pentru aplicații iOS.

Introducere: De Ce Concurența în Swift E un Subiect pe Care Nu Îl Puteți Ignora

Gândiți-vă la ultima aplicație pe care ați folosit-o. Ați derulat un feed, o imagine s-a încărcat în fundal, o notificare a apărut, iar animația de scroll a rămas perfect fluidă. Toate astea s-au întâmplat simultan. Asta e concurența — capacitatea aplicației de a jongla cu mai multe operații în același timp, fără să blocheze interfața.

Sincer, înainte de Swift 5.5, concurența în iOS era un teritoriu minat. Aveam Grand Central Dispatch cu cozile sale, callback-uri imbricate (faimosul „callback hell") și o sumedenie de bug-uri subtile legate de accesul simultan la date partajate. Funcționa, da, dar codul era greu de citit, greu de depanat și — hai să fim onești — surprinzător de ușor de scris greșit.

Apoi Apple a schimbat radical jocul cu Swift Structured Concurrency. Nu-i un framework extern și nici un pattern pe care trebuie să-l implementați manual. E parte din Swift însuși, cu suport la nivel de compilator pentru detectarea erorilor de concurență. Și chiar funcționează bine.

În acest ghid parcurgem totul — de la bazele async/await până la Actors, TaskGroup, protocolul Sendable și regulile stricte din Swift 6. Fiecare concept vine cu exemple practice pe care le puteți rula direct în proiectele voastre.

Bazele async/await: Cod Asincron Care Se Citește Ca Cel Sincron

Ce Sunt Funcțiile async

O funcție marcată cu async îi spune compilatorului: „Această funcție ar putea să-și suspende execuția la un moment dat." Nu blochează thread-ul curent — pur și simplu cedează controlul și revine când rezultatul e gata.

func incarcaProfil(utilizatorId: String) async throws -> Profil {
    let url = URL(string: "https://api.exemplu.ro/utilizatori/\(utilizatorId)")!
    let (date, raspuns) = try await URLSession.shared.data(from: url)

    guard let httpRaspuns = raspuns as? HTTPURLResponse,
          httpRaspuns.statusCode == 200 else {
        throw EroareRetea.raspunsInvalid
    }

    return try JSONDecoder().decode(Profil.self, from: date)
}

Observați cuvântul cheie await — acesta marchează punctul exact unde funcția se poate suspenda. Compilatorul știe că după await, execuția ar putea continua pe un alt thread. E o informație crucială pentru scrierea codului corect.

Comparație: Callback-uri vs async/await

Hai să vedem diferența concretă. Iată cum arăta înainte descărcarea unui profil cu validare, urmată de postările utilizatorului:

// Vechea abordare cu callback-uri (completion handlers)
func incarcaDateUtilizator(id: String, completion: @escaping (Result<(Profil, [Postare]), Error>) -> Void) {
    incarcaProfil(id: id) { rezultatProfil in
        switch rezultatProfil {
        case .success(let profil):
            incarcaPostari(utilizatorId: profil.id) { rezultatPostari in
                switch rezultatPostari {
                case .success(let postari):
                    completion(.success((profil, postari)))
                case .failure(let eroare):
                    completion(.failure(eroare))
                }
            }
        case .failure(let eroare):
            completion(.failure(eroare))
        }
    }
}

Și acum varianta cu async/await:

// Abordarea modernă cu async/await
func incarcaDateUtilizator(id: String) async throws -> (Profil, [Postare]) {
    let profil = try await incarcaProfil(utilizatorId: id)
    let postari = try await incarcaPostari(utilizatorId: profil.id)
    return (profil, postari)
}

Diferența e dramatică. Același rezultat, dar codul se citește liniar, de sus în jos, exact ca o funcție sincronă obișnuită. Gestionarea erorilor cu try/catch funcționează natural, fără piramide de callback-uri. Deja nu mai vreau să mă întorc la vechiul mod.

Apelarea Funcțiilor async din Contexte Sincrone

Nu puteți apela o funcție async direct dintr-un context sincron — aveți nevoie de un Task:

struct ProfilView: View {
    @State private var profil: Profil?
    @State private var eroare: String?

    var body: some View {
        VStack {
            if let profil {
                Text(profil.nume)
                    .font(.title)
            } else if let eroare {
                Text(eroare)
                    .foregroundStyle(.red)
            } else {
                ProgressView("Se încarcă...")
            }
        }
        .task {
            do {
                profil = try await incarcaProfil(utilizatorId: "123")
            } catch {
                self.eroare = eroare.localizedDescription
            }
        }
    }
}

Modificatorul .task din SwiftUI creează automat un Task legat de ciclul de viață al view-ului. Când view-ul dispare, task-ul se anulează automat — ceea ce e mult mai sigur decât să creați manual un Task în onAppear. Dacă nu folosiți deja .task în loc de onAppear, faceți schimbarea. Merită.

Task și Concurența Structurată

Crearea Task-urilor

Un Task reprezintă o unitate de lucru asincron. Există două tipuri principale:

// Task legat de contextul curent (moștenește prioritatea și actorul)
Task {
    let date = try await incarcaDate()
    await actualizeazaUI(cu: date)
}

// Task detașat (independent de contextul părinte)
Task.detached(priority: .background) {
    await proceseazaDateMari()
}

Diferența e importantă: un Task obișnuit moștenește contextul actorului părinte (de exemplu, @MainActor), pe când Task.detached rulează complet independent. În practică, o să folosiți Task.detached destul de rar — și probabil e mai bine așa.

Anularea Task-urilor

Anularea e un aspect al concurenței pe care mulți dezvoltatori îl neglijează. Am văzut asta de prea multe ori în code review-uri.

Task-urile pot fi anulate, dar anularea e cooperativă — trebuie să verificați activ dacă task-ul a fost anulat:

func proceseazaElementeMasiv(_ elemente: [Element]) async throws -> [Rezultat] {
    var rezultate: [Rezultat] = []

    for element in elemente {
        // Verifică dacă task-ul a fost anulat
        try Task.checkCancellation()

        let rezultat = await proceseaza(element)
        rezultate.append(rezultat)
    }

    return rezultate
}

// Alternativ, verificare non-throwing
func proceseazaCuGratie(_ elemente: [Element]) async -> [Rezultat] {
    var rezultate: [Rezultat] = []

    for element in elemente {
        if Task.isCancelled {
            break // Oprește procesarea, returnează ce avem
        }
        let rezultat = await proceseaza(element)
        rezultate.append(rezultat)
    }

    return rezultate
}

async let — Concurență Paralelă Simplă

Când aveți mai multe operații independente care nu depind una de alta, le puteți rula în paralel cu async let. Ăsta-i probabil unul din cele mai satisfăcătoare lucruri din Swift modern:

func incarcaDashboard() async throws -> Dashboard {
    // Toate trei încep simultan!
    async let profil = incarcaProfil(utilizatorId: "123")
    async let postari = incarcaPostari(utilizatorId: "123")
    async let notificari = incarcaNotificari()

    // Așteptăm toate rezultatele
    return try await Dashboard(
        profil: profil,
        postari: postari,
        notificari: notificari
    )
}

Fără async let, cele trei apeluri s-ar executa secvențial — unul după altul. Cu async let, toate trei pornesc imediat, iar timpul total e determinat de cel mai lent apel, nu de suma lor. Într-un scenariu real, asta poate reduce timpul de încărcare al unui ecran de la 3 secunde la 1 secundă. Serios, e o diferență pe care utilizatorii o simt.

TaskGroup: Concurență Dinamică la Scară

Când async let Nu E Suficient

async let e perfect când știți la compilare câte operații paralele aveți. Dar ce faceți când numărul e dinamic — de exemplu, trebuie să descărcați 50 de imagini în paralel? Aici intervine TaskGroup.

func descarcaImagini(urluri: [URL]) async throws -> [URL: UIImage] {
    try await withThrowingTaskGroup(of: (URL, UIImage?).self) { grup in
        for url in urluri {
            grup.addTask {
                let (date, _) = try await URLSession.shared.data(from: url)
                let imagine = UIImage(data: date)
                return (url, imagine)
            }
        }

        var rezultate: [URL: UIImage] = [:]
        for try await (url, imagine) in grup {
            if let imagine {
                rezultate[url] = imagine
            }
        }
        return rezultate
    }
}

Controlul Gradului de Paralelism

Acum, un aspect pe care multe tutoriale îl ignoră complet: dacă aveți 1000 de URL-uri, chiar nu vreți să lansați 1000 de cereri simultan. O să vă supraîncărcați serverul, o să consumați toată memoria, și probabil o să fiți și blocat de rate limiting. Iată cum limitați concurența:

func descarcaCuLimita(urluri: [URL], limitaConcurenta: Int = 5) async throws -> [Data] {
    try await withThrowingTaskGroup(of: (Int, Data).self) { grup in
        var rezultate = Array(repeating: nil, count: urluri.count)
        var index = 0

        // Lansează primele N task-uri
        for _ in 0..

Acest pattern de „fereastră glisantă" menține exact limitaConcurenta cereri active în orice moment. E esențial pentru aplicații de producție — fără el, lucrurile pot merge prost destul de repede.

Actors: Protecția Datelor Partajate

Problema Fundamentală: Data Races

Un data race apare când două thread-uri accesează aceleași date simultan, iar cel puțin unul le modifică. Rezultatul? Imprevizibil — crash-uri aleatorii, date corupte, bug-uri pe care nu le poți reproduce nici dacă vrei. Cu GCD, evitarea data race-urilor depindea complet de disciplina programatorului (și toți știm cum se termină asta).

Actors rezolvă problema la nivel de limbaj.

// GREȘIT: Clasă obișnuită accesată din mai multe task-uri
class ContorNesigur {
    var valoare = 0

    func incrementeaza() {
        valoare += 1 // Data race dacă apelat din task-uri diferite!
    }
}

// CORECT: Actor care protejează automat datele
actor ContorSigur {
    var valoare = 0

    func incrementeaza() {
        valoare += 1 // Acces serializat automat — imposibil data race
    }
}

Cum Funcționează Actors

Un actor e similar cu o clasă, dar cu o garanție fundamentală: doar un singur task poate accesa starea internă a actorului la un moment dat. Orice acces din exterior necesită await, pentru că ar putea fi nevoie să aștepte până când actorul devine disponibil.

Să vedem un exemplu mai realist — un cache de imagini:

actor CacheImagini {
    private var cache: [URL: UIImage] = [:]
    private var incarcareCurenta: [URL: Task] = [:]

    func imagine(pentru url: URL) async throws -> UIImage {
        // Verifică cache-ul
        if let imagineCacheata = cache[url] {
            return imagineCacheata
        }

        // Verifică dacă există deja o încărcare în curs
        if let taskExistent = incarcareCurenta[url] {
            return try await taskExistent.value
        }

        // Creează un task nou de încărcare
        let task = Task {
            let (date, _) = try await URLSession.shared.data(from: url)
            guard let imagine = UIImage(data: date) else {
                throw EroareImagine.dateInvalide
            }
            return imagine
        }

        incarcareCurenta[url] = task

        do {
            let imagine = try await task.value
            cache[url] = imagine
            incarcareCurenta.removeValue(forKey: url)
            return imagine
        } catch {
            incarcareCurenta.removeValue(forKey: url)
            throw error
        }
    }

    func golesteCacheul() {
        cache.removeAll()
    }
}

// Utilizare
let cache = CacheImagini()
let imagine = try await cache.imagine(pentru: urlImagine)

Fiecare apel către cache.imagine(pentru:) necesită await deoarece actorul ar putea fi ocupat cu altă cerere. Dar în interiorul actorului, codul e sincron — nu aveți nevoie de await pentru a accesa cache sau incarcareCurenta. Actorul garantează că nimeni altcineva nu le atinge în acel moment. Destul de elegant, nu?

nonisolated — Proprietăți și Metode Neprotejate

Uneori aveți proprietăți sau metode care nu accesează starea mutabilă a actorului. Le puteți marca ca nonisolated pentru a evita overhead-ul inutil:

actor ServiciuRetea {
    let urlDeBaza: URL
    private var token: String?

    init(urlDeBaza: URL) {
        self.urlDeBaza = urlDeBaza
    }

    // Nu necesită await — urlDeBaza e let (imutabil)
    nonisolated func construiesteURL(cale: String) -> URL {
        urlDeBaza.appendingPathComponent(cale)
    }

    // Necesită await — accesează token (mutabil)
    func faceCerere(cale: String) async throws -> Data {
        var request = URLRequest(url: construiesteURL(cale: cale))
        if let token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        let (date, _) = try await URLSession.shared.data(for: request)
        return date
    }
}

@MainActor: Actualizări UI Garantate pe Thread-ul Principal

De Ce E Necesar

Regulă de aur în iOS (și probabil cea mai importantă regulă de reținut): orice actualizare a interfeței trebuie să se facă pe thread-ul principal. @MainActor e un actor global care garantează exact asta. Puteți să-l aplicați la metode individuale, proprietăți sau clase întregi:

@MainActor
@Observable
class DashboardViewModel {
    var profil: Profil?
    var postari: [Postare] = []
    var esteIncarcare = false
    var mesajEroare: String?

    func incarcaDate() async {
        esteIncarcare = true
        defer { esteIncarcare = false }

        do {
            async let profilTask = ServiciuAPI.shared.incarcaProfil()
            async let postariTask = ServiciuAPI.shared.incarcaPostari()

            let (profil, postari) = try await (profilTask, postariTask)

            self.profil = profil
            self.postari = postari
        } catch {
            mesajEroare = eroare(error)
        }
    }

    private func eroare(_ error: Error) -> String {
        switch error {
        case EroareRetea.faraConexiune:
            return "Nu există conexiune la internet"
        case EroareRetea.timeout:
            return "Cererea a expirat. Încercați din nou."
        default:
            return "Eroare neașteptată: \(error.localizedDescription)"
        }
    }
}

Deoarece întreaga clasă e marcată @MainActor, toate proprietățile și metodele rulează automat pe thread-ul principal. Actualizările UI — setarea esteIncarcare, postari — sunt garantat sigure. Fără griji.

MainActor.run — Execuție Punctuală pe Main Thread

Când aveți nevoie să executați doar o porțiune de cod pe main thread (nu întreaga clasă), folosiți MainActor.run:

func proceseazaInFundal() async {
    let dateProcesat = await proceseazaDateMasiv()

    await MainActor.run {
        // Doar această parte rulează pe main thread
        self.rezultate = dateProcesat
        self.esteGata = true
    }
}

Sendable: Siguranța Datelor între Granițele Concurenței

Ce Înseamnă Sendable

Protocolul Sendable marchează tipurile care pot fi trimise în siguranță între task-uri sau actori diferiți. Practic, e garanția compilatorului că datele respective nu vor cauza data race-uri.

// Structurile cu proprietăți Sendable sunt automat Sendable
struct Coordonate: Sendable {
    let latitudine: Double
    let longitudine: Double
}

// Clasele trebuie să fie finale și cu proprietăți imutabile
final class ConfiguratieApp: Sendable {
    let urlAPI: URL
    let versiune: String
    let esteDebug: Bool

    init(urlAPI: URL, versiune: String, esteDebug: Bool) {
        self.urlAPI = urlAPI
        self.versiune = versiune
        self.esteDebug = esteDebug
    }
}

// Enum-urile sunt automat Sendable dacă valorile asociate sunt Sendable
enum StareIncarcare: Sendable {
    case inactiv
    case incarcare
    case succes(Coordonate)
    case eroare(String)
}

@Sendable Closures

Când pasați closure-uri între granițele de concurență, compilatorul verifică dacă sunt @Sendable — adică nu captează referințe mutabile care ar putea crea probleme:

actor Procesor {
    func proceseaza(elemente: [Int], transformare: @Sendable (Int) -> Int) -> [Int] {
        elemente.map(transformare)
    }
}

// Funcționează — closure-ul nu captează nimic mutabil
let procesor = Procesor()
let rezultate = await procesor.proceseaza(elemente: [1, 2, 3]) { $0 * 2 }

// NU funcționează în Swift 6 — ar captura o variabilă mutabilă
var multiplicator = 2
let rezultateGresite = await procesor.proceseaza(elemente: [1, 2, 3]) { $0 * multiplicator } // Eroare!

@unchecked Sendable — Ultima Soluție

Când știți că un tip e sigur dar compilatorul nu poate verifica asta (poate pentru că folosiți un mecanism propriu de sincronizare), puteți folosi @unchecked Sendable:

final class CacheSincronizat: @unchecked Sendable {
    private let coada = DispatchQueue(label: "ro.exemplu.cache", attributes: .concurrent)
    private var stocare: [String: Any] = [:]

    func obtine(_ cheie: String) -> Any? {
        coada.sync { stocare[cheie] }
    }

    func seteaza(_ cheie: String, valoare: Any) {
        coada.async(flags: .barrier) { [self] in
            stocare[cheie] = valoare
        }
    }
}

Un avertisment serios aici: @unchecked Sendable dezactivează verificările compilatorului. Dacă tipul nu e cu adevărat thread-safe, veți avea data race-uri pe care nimeni nu le va mai detecta automat. Folosiți-l doar când sunteți absolut siguri — și lăsați un comentariu care explică de ce e sigur. Viitorul vostru „eu" vă va mulțumi.

AsyncSequence și AsyncStream: Secvențe de Valori Asincrone

Iterarea Asincronă

AsyncSequence e echivalentul asincron al protocolului Sequence. Permite iterarea peste valori care sosesc în timp — perfect pentru stream-uri de date, actualizări în timp real sau procesarea fișierelor mari linie cu linie:

// Citirea unui fișier linie cu linie — fără a încărca totul în memorie
func citesteFisierMare(url: URL) async throws -> [String] {
    var linii: [String] = []

    for try await linie in url.lines {
        if !linie.isEmpty {
            linii.append(linie)
        }
    }

    return linii
}

// Monitorizarea notificărilor de sistem
func monitorizareNotificari() async {
    let notificari = NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification)

    for await notificare in notificari {
        print("Aplicația a devenit activă: \(notificare)")
        await actualizeazaDate()
    }
}

Crearea Propriilor AsyncStream

AsyncStream vă permite să creați propriile secvențe asincrone — foarte util când trebuie să adaptați API-uri bazate pe delegate sau callback-uri la lumea async/await:

class MonitorLocatie: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    func locatiiLive() -> AsyncStream {
        AsyncStream { continuation in
            self.continuare = continuation
            manager.delegate = self
            manager.startUpdatingLocation()

            continuation.onTermination = { [weak self] _ in
                self?.manager.stopUpdatingLocation()
            }
        }
    }

    private var continuare: AsyncStream.Continuation?

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        for locatie in locations {
            continuare?.yield(locatie)
        }
    }
}

// Utilizare într-un View
struct HartaView: View {
    let monitor = MonitorLocatie()
    @State private var locatieCurenta: CLLocation?

    var body: some View {
        Map()
            .task {
                for await locatie in monitor.locatiiLive() {
                    locatieCurenta = locatie
                }
            }
    }
}

Continuări: Punte între Lumea Veche și Cea Nouă

withCheckedContinuation și withCheckedThrowingContinuation

Aveți un API mai vechi bazat pe callback-uri pe care vreți să-l folosiți cu async/await? Continuările sunt exact ce vă trebuie:

// API vechi cu callback
func incarcaDateVechi(completion: @escaping (Result) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error {
            completion(.failure(error))
        } else if let data {
            completion(.success(data))
        }
    }.resume()
}

// Adaptat pentru async/await
func incarcaDateModern() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        incarcaDateVechi { rezultat in
            continuation.resume(with: rezultat)
        }
    }
}

O regulă critică aici: continuarea trebuie reluată (resume) exact o singură dată. Dacă nu o reluați niciodată, task-ul va fi suspendat pentru totdeauna. Dacă o reluați de două ori, aplicația va face crash. Varianta withCheckedContinuation verifică aceste condiții în debug și vă avertizează — deci folosiți-o în loc de withUnsafeContinuation până când sunteți siguri că totul funcționează corect.

Swift 6: Concurență Strictă și Migrarea Codului Existent

Ce Se Schimbă în Swift 6

Asta-i o schimbare mare. Swift 6 transformă avertismentele de concurență în erori de compilare. Orice potențial data race detectat de compilator vă va împiedica să construiți proiectul. Sună dur, dar e o schimbare necesară — elimină o întreagă categorie de bug-uri care altfel v-ar fi mâncat ore întregi de debugging.

// În Swift 6, acesta NU compilează:
class ServiciuVechi {
    var dateCache: [String: Data] = [:] // Stare mutabilă nepartajată

    func incarcaDate() async {
        Task.detached {
            self.dateCache["cheie"] = Data() // Eroare: data race potențial
        }
    }
}

// Soluția: folosiți un actor
actor ServiciuModern {
    var dateCache: [String: Data] = [:]

    func incarcaDate() async {
        dateCache["cheie"] = Data() // Sigur — actorul serializează accesul
    }
}

Migrarea Incrementală

Vestea bună: nu trebuie să migrați tot codul dintr-o dată. Swift oferă opțiuni de migrare incrementală prin setările Xcode sau Package.swift:

// În Package.swift, puteți activa verificarea strictă per target
.target(
    name: "ModulNou",
    swiftSettings: [
        .swiftLanguageMode(.v6) // Verificare strictă
    ]
),
.target(
    name: "ModulVechi",
    swiftSettings: [
        .swiftLanguageMode(.v5) // Modul de compatibilitate
    ]
)

Strategia recomandată: activați Swift 6 pe module noi și migrați treptat modulele existente, începând cu cele mai simple. Avertismentele din Swift 5 mode (cu -strict-concurrency=complete) vă arată exact ce trebuie reparat înainte de a face saltul. E un proces, nu un eveniment — tratați-l ca atare.

Exemplu Practic Complet: Aplicație de Știri cu Concurență

Hai să punem totul cap la cap într-un exemplu realist — o aplicație de știri care încarcă articole din mai multe surse, le procesează concurent și afișează rezultatele. Ăsta-i genul de cod pe care l-ați scrie efectiv într-un proiect real:

// Modelele
struct Articol: Identifiable, Sendable {
    let id: UUID
    let titlu: String
    let rezumat: String
    let sursa: String
    let dataPublicarii: Date
}

enum SursaStiri: String, CaseIterable, Sendable {
    case tehnologie = "tech"
    case stiinta = "science"
    case sport = "sport"
}

// Actorul pentru cache
actor CacheArticole {
    private var articoleCacheate: [SursaStiri: [Articol]] = [:]
    private var ultimaActualizare: [SursaStiri: Date] = [:]

    func obtineArticole(sursa: SursaStiri) -> [Articol]? {
        guard let data = ultimaActualizare[sursa],
              Date().timeIntervalSince(data) < 300 else { // Cache 5 minute
            return nil
        }
        return articoleCacheate[sursa]
    }

    func salveazaArticole(_ articole: [Articol], sursa: SursaStiri) {
        articoleCacheate[sursa] = articole
        ultimaActualizare[sursa] = Date()
    }
}

// Serviciul principal
actor ServiciuStiri {
    private let cache = CacheArticole()

    func incarcaDinSursa(_ sursa: SursaStiri) async throws -> [Articol] {
        // Verifică cache-ul mai întâi
        if let cacheate = await cache.obtineArticole(sursa: sursa) {
            return cacheate
        }

        // Simulăm un apel de rețea
        try await Task.sleep(for: .milliseconds(500))

        let articole = (0..<10).map { i in
            Articol(
                id: UUID(),
                titlu: "Articol \(i) din \(sursa.rawValue)",
                rezumat: "Rezumat pentru articolul \(i)",
                sursa: sursa.rawValue,
                dataPublicarii: Date()
            )
        }

        await cache.salveazaArticole(articole, sursa: sursa)
        return articole
    }

    func incarcaDinToateSursele() async throws -> [Articol] {
        try await withThrowingTaskGroup(of: [Articol].self) { grup in
            for sursa in SursaStiri.allCases {
                grup.addTask {
                    try await self.incarcaDinSursa(sursa)
                }
            }

            var toateArticolele: [Articol] = []
            for try await articole in grup {
                toateArticolele.append(contentsOf: articole)
            }

            return toateArticolele.sorted { $0.dataPublicarii > $1.dataPublicarii }
        }
    }
}

// ViewModel
@MainActor
@Observable
class StiriViewModel {
    var articole: [Articol] = []
    var esteIncarcare = false
    var eroare: String?

    private let serviciu = ServiciuStiri()

    func incarcaStiri() async {
        esteIncarcare = true
        defer { esteIncarcare = false }

        do {
            articole = try await serviciu.incarcaDinToateSursele()
        } catch {
            self.eroare = error.localizedDescription
        }
    }
}

// View
struct StiriView: View {
    @State private var viewModel = StiriViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.esteIncarcare {
                    ProgressView("Se încarcă știrile...")
                } else {
                    List(viewModel.articole) { articol in
                        VStack(alignment: .leading, spacing: 4) {
                            Text(articol.titlu)
                                .font(.headline)
                            Text(articol.rezumat)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                            Text(articol.sursa.uppercased())
                                .font(.caption)
                                .foregroundStyle(.blue)
                        }
                    }
                }
            }
            .navigationTitle("Știri")
            .task {
                await viewModel.incarcaStiri()
            }
            .refreshable {
                await viewModel.incarcaStiri()
            }
        }
    }
}

Întrebări Frecvente

Care e diferența dintre async/await și Grand Central Dispatch (GCD)?

async/await e o construcție la nivel de limbaj care face codul asincron ușor de citit și de întreținut. GCD e un API de nivel inferior pentru managementul cozilor de dispatch. Principala diferență: async/await oferă concurență structurată — compilatorul înțelege fluxul de execuție, poate detecta erori la compilare, iar anularea și gestionarea erorilor funcționează natural cu try/catch. GCD rămâne disponibil, dar pentru cod nou, async/await e recomandat aproape întotdeauna.

Când ar trebui să folosesc un Actor în loc de o clasă obișnuită?

Folosiți un actor ori de câte ori aveți o clasă cu stare mutabilă care ar putea fi accesată din mai multe task-uri simultan. Exemple tipice: cache-uri, manageri de sesiune, contoare, servicii de rețea cu stare internă. Dacă obiectul e accesat doar dintr-un singur context (de exemplu, numai de pe main thread), o clasă obișnuită sau chiar o structură sunt suficiente.

Ce se întâmplă dacă nu verific anularea într-un Task?

Task-ul va continua să ruleze până la finalizare, consumând resurse degeaba. Anularea în Swift e cooperativă — sistemul setează un flag, dar task-ul trebuie să verifice activ (prin Task.checkCancellation() sau Task.isCancelled) și să răspundă. Operațiile din framework-urile Apple (precum URLSession.shared.data(from:)) respectă automat anularea, dar codul vostru propriu trebuie să o gestioneze explicit.

Pot folosi async/await în proiecte care suportă versiuni mai vechi de iOS?

Da, dar cu limitări. async/await e disponibil începând cu iOS 15 (Swift 5.5). Actors la fel, de la iOS 15. Dacă trebuie să suportați iOS 14 sau mai vechi, va trebui să continuați cu GCD și callback-uri pentru acele secțiuni. Vestea bună e că puteți folosi #available pentru a avea cod diferit pe versiuni diferite, permițând o migrare treptată.

Care e diferența dintre Task și Task.detached?

Task moștenește contextul actorului din care e creat — dacă e creat dintr-un @MainActor, va rula pe main thread. Task.detached nu moștenește nimic și rulează complet independent. În practică, Task.detached e util când vreți să efectuați o operație costisitoare care nu trebuie neapărat să ruleze pe main thread, dar folosiți-l cu precauție — pierde legătura cu actorul părinte și prioritatea contextuală.

Despre Autor Editorial Team

Our team of expert writers and editors.