SwiftData, Swift Concurrency, and AsyncSequence: A Complete Guide for Modern iOS Development
June 17, 2026
Modern iOS development has evolved significantly with the introduction of SwiftData, Swift Concurrency, and AsyncSequence. These technologies help developers write safer, cleaner, and more efficient code while reducing complexity.
Before iOS 17, developers primarily relied on Core Data for persistence and completion handlers for asynchronous programming. Today, Apple provides powerful modern alternatives that integrate seamlessly with Swift and SwiftUI.
In this tutorial, you’ll learn:
- What SwiftData is
- How Swift Concurrency works
- Understanding AsyncSequence
- Real-world examples
- Best practices for production apps

What is SwiftData?
SwiftData is Apple’s modern persistence framework introduced in iOS 17. It simplifies local data storage while maintaining the power of Core Data.
Benefits of SwiftData
- Less boilerplate code
- Native Swift support
- SwiftUI integration
- Automatic data observation
- Easier relationships management
- Better readability
Creating a SwiftData Model
Use the @Model macro to create a persistent model.
import SwiftData
@Model
class Note {
var title: String
var content: String
var createdDate: Date
init(
title: String,
content: String,
createdDate: Date = Date()
) {
self.title = title
self.content = content
self.createdDate = createdDate
}
}
This model automatically becomes persistent.
Setting Up SwiftData
Configure the model container in your app.
import SwiftUI
import SwiftData
@main
struct NotesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Note.self)
}
}
Insert Data
Use ModelContext.
@Environment(\.modelContext)
private var context
func addNote() {
let note = Note(
title: "SwiftData",
content: "Learning SwiftData"
)
context.insert(note)
}
Fetch Data
SwiftData introduces the @Query property wrapper.
@Query
private var notes: [Note]
Display data:
List(notes) { note in
Text(note.title)
}
Filtering Data
@Query(
filter: #Predicate<Note> {
$0.title.contains("Swift")
}
)
var notes: [Note]
Sorting Data
@Query(
sort: \Note.createdDate,
order: .reverse
)
var notes: [Note]
Deleting Data
func deleteNote(_ note: Note) {
context.delete(note)
}
What is Swift Concurrency?
Swift Concurrency is Apple’s modern asynchronous programming model introduced in Swift 5.5.
It replaces:
- Completion Handlers
- Nested Callbacks
- Complex GCD Code
With:
- async/await
- Task
- TaskGroup
- Actor
Why Swift Concurrency?
Traditional code:
fetchData { result in
parseData {
saveData {
updateUI()
}
}
}
This creates Callback Hell.
Swift Concurrency:
let data = try await fetchData()
let parsed = try await parseData(data)
try await saveData(parsed)
updateUI()
Much cleaner and easier to maintain.
Async Function
func fetchUser() async throws -> User {
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
Calling Async Functions
Task {
do {
let user = try await fetchUser()
print(user.name)
} catch {
print(error)
}
}
Task
A Task creates a concurrent unit of work.
Task {
print("Running asynchronously")
}
Task Priority
Task(priority: .high) {
await loadData()
}
Available priorities:
.high
.medium
.low
.background
Running Parallel Tasks
async let user = fetchUser()
async let posts = fetchPosts()
let result = try await (user, posts)
Both operations execute simultaneously.
Task Group
Execute multiple dynamic tasks concurrently.
let images = await withTaskGroup(
of: UIImage?.self
) { group in
for url in imageUrls {
group.addTask {
await downloadImage(url)
}
}
var result = [UIImage]()
for await image in group {
if let image {
result.append(image)
}
}
return result
}
MainActor
UI updates should occur on the main thread.
@MainActor
class UserViewModel: ObservableObject {
@Published var user: User?
func loadUser() async {
user = try? await fetchUser()
}
}
What are Actors?
Actors protect shared mutable state.
Without Actor:
class Counter {
var value = 0
func increment() {
value += 1
}
}
Multiple threads can create race conditions.
Actor Example
actor Counter {
private var value = 0
func increment() {
value += 1
}
func currentValue() -> Int {
value
}
}
Usage:
let counter = Counter()
await counter.increment()
let value = await counter.currentValue()
Actors ensure thread safety automatically.
What is AsyncSequence?
AsyncSequence is the asynchronous version of Sequence.
Normal Sequence:
for item in items {
print(item)
}
AsyncSequence:
for await item in stream {
print(item)
}
Values arrive over time instead of being immediately available.
Why AsyncSequence?
Useful for:
- Real-time chat
- Location updates
- Notifications
- Network streaming
- Sensor data
- Download progress
AsyncSequence Example
struct NumberSequence: AsyncSequence {
typealias Element = Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 1
mutating func next() async -> Int? {
guard current <= 5 else {
return nil
}
try? await Task.sleep(
for: .seconds(1)
)
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator()
}
}
Usage:
let sequence = NumberSequence()
for await number in sequence {
print(number)
}
Output:
1
2
3
4
5
AsyncStream
The easiest way to create AsyncSequence.
let stream = AsyncStream<Int> { continuation in
Task {
for i in 1...5 {
continuation.yield(i)
try? await Task.sleep(
for: .seconds(1)
)
}
continuation.finish()
}
}
Consume:
for await value in stream {
print(value)
}
Real-World Example: Download Progress
func downloadFile() -> AsyncStream<Double> {
AsyncStream { continuation in
Task {
for progress in stride(
from: 0.0,
through: 1.0,
by: 0.1
) {
continuation.yield(progress)
try? await Task.sleep(
for: .milliseconds(500)
)
}
continuation.finish()
}
}
}
Usage:
for await progress in downloadFile() {
print(progress)
}
Combining SwiftData with Concurrency
@MainActor
class NotesViewModel: ObservableObject {
@Environment(\.modelContext)
var context
func syncNotes() async {
do {
let remoteNotes = try await api.fetchNotes()
for note in remoteNotes {
context.insert(
Note(
title: note.title,
content: note.content
)
)
}
} catch {
print(error)
}
}
}
This approach keeps UI responsive while syncing data.
Best Practices
SwiftData
✔ Use @Model
✔ Use @Query for fetching
✔ Keep models simple
✔ Create indexes for large datasets
Swift Concurrency
✔ Prefer async/await over completion handlers
✔ Use Actors for shared state
✔ Use MainActor for UI updates
✔ Cancel unnecessary tasks
AsyncSequence
✔ Use AsyncStream for custom streams
✔ Always call finish()
✔ Handle task cancellation
✔ Avoid memory leaks
When Should You Use These Technologies?
| Technology | Use Case |
|---|---|
| SwiftData | Local Database |
| Async/Await | Network Calls |
| TaskGroup | Parallel Operations |
| Actor | Thread Safety |
| AsyncSequence | Streaming Data |
| AsyncStream | Custom Event Streams |
Conclusion
SwiftData, Swift Concurrency, and AsyncSequence represent the future of iOS development. Together, they simplify data management, asynchronous programming, and real-time event handling.
By mastering these technologies, you can build applications that are:
- Faster
- More responsive
- Easier to maintain
- Thread-safe
- Scalable
Whether you’re creating a note-taking app, social platform, e-commerce application, or AI-powered product, these modern Swift features should be part of your development toolkit.