בידוד אקטור ברירת מחדל ב-Swift 6.2: המדריך המלא ל-Approachable Concurrency

Swift 6.2 משנה את כללי המשחק עם בידוד אקטור ברירת מחדל ל-@MainActor. מדריך מעשי שמראה איך להפעיל את הפיצ'ר, להעביר פרויקטים קיימים ולפתור שגיאות פרוטוקול נפוצות — עם דוגמאות קוד מלאות.

Swift 6.2 @MainActor: מדריך מלא 2026

אז Swift 6.2 כבר כאן, ולמען האמת — זה לא סתם עוד עדכון נקודה. השחרור של 2026 הביא איתו שינוי שמרגיש כמעט פילוסופי באופן שבו אנחנו כותבים קוד קונקרנטי באפליקציות iOS. בלב העניין נמצא Default Actor Isolation, פיצ'ר שהופך את @MainActor לברירת המחדל ומבטל בערך 80% מהאנוטציות שהיו נדרשות בעבר (לפחות בפרויקטים שלי).

במדריך הזה נצלול לעומק של "Approachable Concurrency", נראה איך מעבירים פרויקט קיים בלי לאבד שפיות, ונטפל בשגיאות הפרוטוקול הנפוצות שכמעט תמיד צצות במהלך המעבר.

למה בעצם Apple שינתה את ברירת המחדל?

עד Swift 6.1, כל הצהרה ללא אנוטציית בידוד הניחה nonisolated. נשמע ניטרלי, נכון? בפועל, ההנחה הזו גרמה למפתחים לראות הררים של שגיאות sendability ברגע שהפעילו Strict Concurrency. אני אישית זוכר פרויקט שבו פשוט סגרתי את Xcode וחזרתי כעבור שבוע — היו שם 240+ אזהרות.

צוות השפה זיהה את הבעיה במסמך החזון מפברואר 2025: רוב אפליקציות ה-iOS חיות ממילא על ה-main actor. אז למה לכפות על המפתח להוכיח את זה בכל מקום מחדש?

הפתרון נקרא Progressive Disclosure. Swift תבקש ממך להבין רק כמה קונקרנטיות שאתה באמת משתמש בה. אם האפליקציה שלך single-threaded ברובה (וכן, רוב האפליקציות כאלה), הקוד שלך יראה כמו קוד סינכרוני רגיל וימשיך ליהנות מבטיחות הקומפיילר.

הפעלת Default Actor Isolation בפרויקט שלך

ב-Xcode 26

פתח את Build Settings, חפש את "Default Actor Isolation" והגדר אותו ל-MainActor. שים לב: ההגדרה הזו זמינה רק כאשר Swift Language Version מוגדר ל-6.2 ומעלה. אם אתה לא מוצא אותה, סביר להניח שזו הסיבה.

ב-Swift Package Manager

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MyApp",
    targets: [
        .target(
            name: "MyAppCore",
            swiftSettings: [
                .defaultIsolation(MainActor.self),
                .enableUpcomingFeature("InferIsolatedConformances")
            ]
        )
    ]
)

חשוב לוודא ש-swift-tools-version מוגדר ל-6.2, אחרת ה-API של defaultIsolation פשוט לא יהיה זמין — וזה ייתן לך שגיאה די מבלבלת.

איך זה משנה את הקוד היומיומי שלך

נסתכל על דוגמה פשוטה. לפני Swift 6.2, ViewModel טיפוסי נראה ככה:

@MainActor
final class UserViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func loadUsers() async {
        isLoading = true
        defer { isLoading = false }
        users = await UserService.fetchAll()
    }
}

אחרי הפעלת Default Actor Isolation, אותו קוד הופך לקצר ונקי יותר:

@Observable
final class UserViewModel {
    var users: [User] = []
    var isLoading = false

    func loadUsers() async {
        isLoading = true
        defer { isLoading = false }
        users = await UserService.fetchAll()
    }
}

אין יותר אנוטציית @MainActor, אין יותר @Published, ועדיין הקוד רץ על ה-main actor עם בטיחות מלאה של data race. הרגשתי בפעם הראשונה שניקיתי 30 שורות מקובץ — קצת מוזר, קצת משחרר.

nonisolated async — השינוי הנסתר שמשנה הכל

אחד השינויים החשובים ביותר ב-Swift 6.2 (וגם הכי פחות מדוברים) הוא שפונקציות nonisolated async יורשות כעת את הבידוד של הקורא. בעבר, אם הייתה לך פונקציה כזו, היא הייתה קופצת ל-cooperative pool בלי לשאול. עכשיו היא תישאר על ה-main actor אם נקראת משם:

nonisolated func fetchData() async throws -> Data {
    // ב-Swift 6.2 הקוד הזה ירוץ על main actor
    // אם נקרא מתוך View או ViewModel main-actor-isolated
    let url = URL(string: "https://api.example.com/data")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

ואם אתה דווקא רוצה את ההתנהגות ה"ישנה" של קפיצה ל-thread pool? יש בשבילך @concurrent:

@concurrent
func processLargeImage(_ image: CGImage) async -> CGImage {
    // כפיית הרצה מחוץ לקורא, על cooperative thread pool
    return imageProcessor.applyFilters(to: image)
}

Typed Throws — בטיחות שגיאות בזמן קומפילציה

פיצ'ר שני שהתקבע ב-Swift 6.2 הוא Typed Throws. במקום לזרוק Error כללי (ולסבול בצד השני), אפשר להצהיר במדויק על סוג השגיאה:

enum NetworkError: Error {
    case noConnection
    case invalidResponse(statusCode: Int)
    case decodingFailed(underlying: Error)
}

func fetchUser(id: UUID) async throws(NetworkError) -> User {
    guard NetworkMonitor.isReachable else {
        throw .noConnection
    }
    let response = try await api.get("/users/\(id)")
    guard response.statusCode == 200 else {
        throw .invalidResponse(statusCode: response.statusCode)
    }
    do {
        return try JSONDecoder().decode(User.self, from: response.body)
    } catch {
        throw .decodingFailed(underlying: error)
    }
}

הקורא יכול עכשיו לטפל בכל מקרה בנפרד, וה-compiler יוודא שלא שכחת אף תרחיש — וזה, לדעתי, הדבר הכי שווה בפיצ'ר כולו:

do {
    let user = try await fetchUser(id: userId)
} catch .noConnection {
    showOfflineBanner()
} catch .invalidResponse(let code):
    logger.error("HTTP \(code)")
} catch .decodingFailed(let underlying):
    Crashlytics.record(error: underlying)
}

פתרון שגיאות פרוטוקול נפוצות

השגיאה הכי שכיחה אחרי המעבר ל-Swift 6.2 — ואני מבטיח, היא תופיע אצלך — היא:

Main actor-isolated instance method cannot be used to satisfy a nonisolated protocol requirement

השגיאה הזו מופיעה כאשר ה-ViewModel שלך (שעכשיו @MainActor בברירת מחדל) מנסה לממש פרוטוקול שלא הוגדר עם בידוד. הפתרון? עדכון הפרוטוקול:

// לפני - גורם לשגיאה
protocol DataSource {
    func reload() async throws
}

// אחרי - מתאים לבידוד החדש
@MainActor
protocol DataSource {
    func reload() async throws
}

אם הפרוטוקול חייב להישאר ניטרלי (למשל ספרייה חיצונית שאתה לא שולט בה), השתמש ב-nonisolated מפורש על המתודה הרלוונטית:

extension UserViewModel: DataSource {
    nonisolated func reload() async throws {
        let fresh = try await UserService.fetchAll()
        await MainActor.run {
            self.users = fresh
        }
    }
}

Region-Based Isolation — הסיבה שזה בכלל עובד

הקסם שמאפשר ל-Approachable Concurrency להרגיש נעים הוא Region-Based Isolation. הקומפיילר מנתח אילו ערכים זורמים בין גבולות בידוד, ודורש Sendable רק כשבאמת יש סיכון ל-race. בפועל, פרויקטים שעברו ל-Swift 6.2 מדווחים על ירידה של 50%-70% במספר האנוטציות Sendable שנדרשות. הפרויקט שלי? ירד מ-87 ל-22. אני עדיין מנסה לעכל את זה.

מתי כדאי לא להפעיל את הפיצ'ר

למרות שהפיצ'ר נהדר לרוב אפליקציות ה-iOS, יש מקרים שבהם עדיף להישאר עם ההתנהגות הישנה:

  • ספריות מערכת: אם אתה כותב SDK שאמור לרוץ בכל הקשר, ברירת מחדל של MainActor פשוט תגביל את הצרכנים שלך.
  • שרתי Vapor: קוד server-side בדרך כלל לא רץ על main actor כלל, אז ברירת המחדל החדשה לא רלוונטית — ועלולה לבלבל את הצוות.
  • חישוב כבד: אם רוב הקוד שלך הוא עיבוד תמונה, ML או חישוב נומרי, מוטב להישאר nonisolated כברירת מחדל.

מדריך מעבר שלב אחר שלב

  1. שדרג ל-Xcode 26 וודא ש-Swift Language Version מוגדר ל-6.2.
  2. הפעל את הפיצ'ר על target בודד תחילה — לא על כל הפרויקט בבת אחת. זה חשוב.
  3. הרץ build ותקן שגיאות פרוטוקול לפי הסעיף למעלה.
  4. הסר אנוטציות @MainActor מיותרות — הן עכשיו ברירת מחדל ופשוט מיותרות.
  5. בדוק שכל פונקציות ה-async הכבדות מסומנות ב-@concurrent אם אתה רוצה שהן יקפצו מה-main actor.
  6. הרץ את חבילת הטסטים. שים לב במיוחד לבדיקות שמסתמכות על קפיצות thread — שם הסיכוי לרגרסיות הכי גבוה.

שאלות נפוצות

האם Default Actor Isolation תפגע בביצועי האפליקציה שלי?

במקרה הכללי, לא. רוב הקוד באפליקציית iOS ממילא רץ על main actor (UI, רוב ה-ViewModels, ה-State). הפיצ'ר רק חוסך אנוטציות; הוא לא מוסיף עבודה בזמן ריצה. עבור חישובים כבדים שכן צריכים thread pool, השתמש ב-@concurrent.

האם אני יכול להפעיל את זה על חלק מהפרויקט בלבד?

כן, וזו דווקא הגישה המומלצת. ב-SPM אתה מגדיר את .defaultIsolation per-target. ב-Xcode אתה יכול להגדיר את ה-Build Setting ברמת ה-Target ולא ברמת ה-Project.

מה ההבדל בין nonisolated לבין @concurrent?

ב-Swift 6.2, nonisolated async יורש את הבידוד של הקורא — אם נקראת מ-main actor, הפונקציה תרוץ על main actor. @concurrent, לעומת זאת, מכריח את הפונקציה לרוץ על cooperative thread pool בלי קשר למי שקרא לה. השתמש ב-@concurrent רק כשאתה באמת רוצה לפנות את ה-main thread.

האם Typed Throws עובד עם פרוטוקולים?

כן. אתה יכול להצהיר על סוג שגיאה ספציפי בפרוטוקול: func save() throws(PersistenceError). מימושים יכולים לזרוק את אותו סוג או תת-סוג. שים לב שהפיצ'ר הזה משנה את חתימת ה-ABI, אז במודולים יציבים יש להיזהר (זה למדתי על הדרך הקשה).

האם ObservableObject עדיין נתמך אחרי המעבר?

כן, ObservableObject ממשיך לעבוד, אבל מומלץ לעבור ל-@Observable שמספק עדכוני view ברמת property בודד במקום ברמת כל האובייקט. השילוב של @Observable עם Default Actor Isolation מצמצם משמעותית את כמות הקוד ה-boilerplate ב-ViewModel ממוצע — לפעמים בחצי.

סיכום

Swift 6.2 הופכת את הקונקרנטיות ב-iOS למשהו שמתחילים יכולים לאמץ בלי לטבוע באנוטציות. השילוב של Default Actor Isolation, nonisolated async שיורש בידוד, ו-Typed Throws נותן ל-Swift קצוות חדים יותר — אבל פחות מפחידים.

אם אתה עובד על אפליקציית iOS ב-2026, המעבר הוא לא שאלה של "אם" אלא "מתי". וככל שתעבור מוקדם יותר, פחות חוב טכני תצבור. תאמין לי, אני דחיתי את זה חודש שלם ושילמתי על זה ביוקר.

אודות הכותב Tomasz Wojcik

Tomasz is a Krakow-based iOS engineer with 11 years of Swift experience. He spent four years at Revolut on the Wealth team, where he rewrote the trading charts in SwiftUI and shaved 40% off cold-start time by lazy-loading the analytics SDK. Before Revolut he was at Allegro, Poland's largest e-commerce platform, on the Seller Center iOS team. His specialty is iOS performance work: Instruments deep-dives, memory-graph debugging, and figuring out why your scroll view drops frames only on iPhone SE 2nd-gen. He has contributed patches to swift-syntax and writes a quarterly newsletter for iOS engineers that covers under-discussed APIs like BackgroundTasks and NSFileCoordinator. Tomasz holds the iOS App Development with Swift certification from Apple and occasionally runs paid workshops on Swift concurrency for in-house engineering teams in Europe.