Swift Attributes and Annotations: @available, @MainActor, @Sendable

Swift attributes and annotations add extra meaning to declarations, types, functions, closures, and modules. They let you describe platform availability, compiler behavior, interoperability, concurrency rules, entry points, and performance hints without changing the core logic of your code. In this guide, you will learn what common Swift attributes do, how to write them correctly, when to use them, and which mistakes often cause confusing compiler errors.

Quick answer: Swift attributes are metadata markers written with the @ symbol, such as @available, @MainActor, and @Sendable. They do not replace normal Swift syntax; instead, they tell the compiler, runtime, or language tools how a declaration should behave or be interpreted.

Difficulty: Intermediate

Helpful to know first: basic Swift syntax, functions, types, access control, and the difference between compile-time checks and runtime behavior will make this topic much easier to understand.

1. What Is a Swift Attribute or Annotation?

A Swift attribute is a marker that starts with @ and attaches extra information to a declaration or type. You place attributes before declarations such as structs, classes, enums, functions, properties, parameters, and closures.

Developers often say attributes and annotations interchangeably. In Swift documentation, attribute is the more precise term.

Some attributes are about source compatibility, some are about concurrency safety, and some are mainly for library authors or compiler optimization.

Not every attribute is appropriate in everyday app code. For example, @available and @MainActor are common, while @frozen and @inline are more specialized.

2. Why Swift Attributes Matter

Attributes matter because they let you express rules that plain types and statements cannot express cleanly on their own.

In real projects, these attributes are especially valuable when you are building reusable APIs, supporting multiple Apple platform versions, adopting async code, or exposing Swift code across module boundaries.

They are not decorations to add casually. Each attribute changes how the compiler reasons about your code, so adding one without understanding it can introduce unnecessary restrictions or hide design problems.

3. Basic Syntax or Core Idea

The basic idea is simple: place an attribute before the declaration it modifies.

Single attribute on a function

This example marks a function as available only on newer iOS versions.

@available(iOS 15.0, *)
func refreshData() {
    print("Refreshing data")
}

The attribute sits above the function declaration and changes when the function may be used.

Multiple attributes on one declaration

A declaration can have more than one attribute when each one serves a different purpose.

@MainActor
@available(iOS 15.0, *)
func updateInterface() {
    print("Updating UI-related state")
}

Here, the function is both main-actor isolated and version-limited.

Attribute on a type

Attributes can also apply to an entire type.

@MainActor
final class SessionController {
    var status = "Idle"
}

All instance members of this type are treated as main-actor isolated unless you explicitly say otherwise.

Attribute on a closure type

Some attributes apply to closure types rather than declarations.

let work: @Sendable () -> Void = {
    print("Running concurrent work")
}

This tells Swift the closure must be safe to transfer between concurrency domains.

4. Step-by-Step Examples

Example 1: Using @available to protect version-specific APIs

Use @available when a declaration should only be used on certain OS versions. This is common when adopting APIs introduced in newer SDKs.

@available(iOS 16.0, macOS 13.0, *)
func useModernFeature() {
    print("Using a modern platform feature")
}

func runFeatureIfSupported() {
    if #available(iOS 16.0, macOS 13.0, *) {
        useModernFeature()
    } else {
        print("Fallback for older systems")
    }
}

The declaration attribute @available marks the function itself, while the statement form #available checks availability at runtime before calling it.

This is an important comparison: @available belongs on declarations, while #available is used inside conditions.

Example 2: Using @MainActor for UI-related state

@MainActor is used when code must execute on the main actor, which is especially important for UI-facing state and other main-thread-sensitive logic.

@MainActor
final class ProfileViewModel {
    var name = "Unknown"

    func rename(to newName: String) {
        name = newName
    }
}

Marking the whole type is often cleaner than marking each method individually. It communicates that the entire object is tied to the main actor.

Example 3: Using @Sendable for concurrent closures

A closure that may run concurrently must avoid capturing non-sendable mutable state in unsafe ways. @Sendable tells Swift to enforce these rules more strictly.

func runTask(_ operation: @Sendable () -> Void) {
    operation()
}

let message = "Safe value capture"

runTask {
    print(message)
}

Capturing an immutable string is fine because String is value-based and safe to transfer.

Example 4: Using @main to define the entry point

@main marks the type that starts the program. This is common in command-line tools and app entry declarations.

@main
struct AppLauncher {
    static func main() {
        print("Application started")
    }
}

The compiler uses this type as the entry point instead of looking for older startup patterns.

Example 5: Using @objc when Objective-C runtime access is required

@objc is mainly relevant when Swift declarations must be exposed to Objective-C runtime features, such as selector-based APIs.

import Foundation

class TimerHandler: NSObject {
    @objc func tick() {
        print("Timer fired")
    }
}

You should only use this when runtime exposure is actually needed. Most pure Swift code does not require it.

Example 6: Using @inline as a compiler hint

@inline is an advanced optimization hint. It does not guarantee a specific machine-level result, but it can influence optimization decisions.

@inline(__always)
func square(_ value: Int) -> Int {
    value * value
}

This kind of attribute is usually for performance-sensitive code after measurement, not before.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Using @available but forgetting the runtime check

It is common to mark a declaration as version-limited and then call it unconditionally from code that still runs on older systems.

Problem: The compiler can reject the call because the API may not exist on the deployment target. The declaration is annotated correctly, but the call site is still unsafe.

@available(iOS 16.0, *)
func newFeature() {
    print("New feature")
}

func start() {
    newFeature()
}

Fix: Guard the call with #available when the current runtime might be older than the API requirement.

@available(iOS 16.0, *)
func newFeature() {
    print("New feature")
}

func start() {
    if #available(iOS 16.0, *) {
        newFeature()
    } else {
        print("Fallback path")
    }
}

The corrected version works because the newer API is only called when the current system supports it.

Mistake 2: Accessing @MainActor state from a nonisolated context incorrectly

Beginners often assume @MainActor is just documentation, but it is enforced by Swift concurrency rules.

Problem: Accessing main-actor-isolated members from another concurrency context can produce isolation errors such as actor-related compile-time diagnostics.

@MainActor
final class Counter {
    var value = 0
}

func printValue(_ counter: Counter) {
    print(counter.value)
}

Fix: Access the isolated member from a main-actor context or use an async hop when appropriate.

@MainActor
final class Counter {
    var value = 0
}

@MainActor
func printValue(_ counter: Counter) {
    print(counter.value)
}

The corrected version works because both the type and the function are isolated to the same actor.

Mistake 3: Capturing unsafe mutable state in an @Sendable closure

@Sendable closures are stricter than ordinary closures. They cannot freely capture mutable reference-based state that may be unsafe across concurrency boundaries.

Problem: This can trigger sendability diagnostics when Swift detects that the closure captures non-sendable or unsafely shared state.

final class LogStore {
    var entries: [String] = []
}

let store = LogStore()

let job: @Sendable () -> Void = {
    store.entries.append("New item")
}

Fix: Capture sendable values, redesign shared state, or isolate the mutable state behind an actor.

actor LogStore {
    var entries: [String] = []

    func add(_ entry: String) {
        entries.append(entry)
    }
}

The corrected approach works because actor isolation protects the mutable shared state.

Mistake 4: Adding @inline(__always) without measuring performance

Developers sometimes assume forcing inline behavior always makes code faster.

Problem: Overusing inlining hints can increase binary size and may even reduce performance depending on optimization context.

@inline(__always)
func formatMessage(_ name: String) -> String {
    "Hello, \(" + name + ")"
}

Fix: Let the optimizer decide unless profiling shows a real hot-path benefit.

func formatMessage(_ name: String) -> String {
    "Hello, \(" + name + ")"
}

The corrected version works because it avoids unnecessary optimization hints and keeps performance tuning evidence-based.

7. Best Practices

Practice 1: Use the narrowest attribute scope that expresses your intent

If only one method needs version gating or runtime exposure, annotate that method rather than the entire type.

// Less preferred when only one member needs the attribute
@MainActor
final class ReportBuilder {
    func build() -> String {
        "report"
    }

    func publishUIStatus() {
        print("Published")
    }
}
// Preferred when only one method is UI-related
final class ReportBuilder {
    func build() -> String {
        "report"
    }

    @MainActor
    func publishUIStatus() {
        print("Published")
    }
}

This keeps isolation and constraints focused on the code that actually needs them.

Practice 2: Prefer @available for declarations and #available for call-site branching

These two forms solve related but different problems. Use the declaration attribute to describe support, and the conditional check to decide what to run at runtime.

@available(macOS 13.0, *)
func modernPath() {
    print("Modern path")
}

func startWork() {
    if #available(macOS 13.0, *) {
        modernPath()
    } else {
        print("Legacy path")
    }
}

This pattern is clear, safe, and easy for other developers to read.

Practice 3: Use @Sendable as an API contract, not just a compiler appeasement tool

If a closure can be executed concurrently, mark it @Sendable in the function signature so callers know the rules.

// Less explicit
func performWork(_ job: () -> Void) {
    job()
}

// Preferred for concurrent-capable APIs
func performWork(_ job: @Sendable () -> Void) {
    job()
}

This makes concurrency expectations part of the API instead of a hidden implementation detail.

Practice 4: Be cautious with library-facing attributes such as @frozen

@frozen is a promise to clients of a public library. Once made, it limits how you can evolve the type.

// Public library design choice
@frozen
public enum Priority {
    case low
    case medium
    case high
}

Use this only when you are confident the public shape is stable enough for long-term compatibility.

8. Limitations and Edge Cases

A useful mental model is that some attributes affect who can call code, some affect where code can run, and others affect how the compiler treats code.

9. Practical Mini Project

This small example combines several attributes in one realistic command-line style program. It uses @main for the entry point, @available for a newer feature, @MainActor for UI-like state isolation, and @Sendable for a closure contract.

import Foundation

@MainActor
final class StatusStore {
    private(set) var status = "Starting"

    func update(_ newStatus: String) {
        status = newStatus
        print("Status: \(" + status + ")")
    }
}

func performBackgroundWork(_ job: @Sendable () -> String) -> String {
    job()
}

@available(macOS 13.0, *)
func modernSummary() -> String {
    "Modern summary feature enabled"
}

@main
struct Program {
    static func main() async {
        let store = StatusStore()
        let result = performBackgroundWork {
            "Background work finished"
        }

        await store.update(result)

        if #available(macOS 13.0, *) {
            print(modernSummary())
        } else {
            print("Legacy summary feature")
        }
    }
}

This example shows how attributes can work together rather than in isolation. The program has a clear entry point, a concurrency-safe closure contract, actor-isolated state updates, and a runtime availability check for a newer feature.

10. Key Points

11. Practice Exercise

Build a small Swift program that practices three different attributes together.

Expected output: A printed message from the closure result, followed by either the modern feature message or a fallback message.

Hint: Keep the closure capture simple by returning a string literal or an immutable constant.

import Foundation

@MainActor
final class MessageStore {
    private(set) var message = "Initial"

    func update(_ newMessage: String) {
        message = newMessage
        print("Stored: \(" + message + ")")
    }
}

func makeMessage(_ builder: @Sendable () -> String) -> String {
    builder()
}

@available(macOS 13.0, *)
func modernFeatureMessage() {
    print("Modern feature is available")
}

@main
struct Runner {
    static func main() async {
        let store = MessageStore()
        let text = makeMessage {
            "Hello from a sendable closure"
        }

        await store.update(text)

        if #available(macOS 13.0, *) {
            modernFeatureMessage()
        } else {
            print("Using fallback behavior")
        }
    }
}

12. Final Summary

Swift attributes and annotations are a powerful part of the language because they let you express information that ordinary syntax cannot express clearly enough on its own. With them, you can describe availability, actor isolation, concurrency safety, entry points, compiler hints, and library evolution rules directly on the declarations they affect.

For most developers, the most important attributes to understand first are @available, @MainActor, @Sendable, and @main. More advanced attributes such as @frozen, @inline, and @objc become more relevant when you work on frameworks, optimization-sensitive code, or interoperability boundaries.

Your best next step is to review a real Swift project and identify where attributes are already in use. Then practice rewriting a few declarations with the correct attribute and call-site checks so the compiler can enforce more of your design for you.