2021 SwiftSwiftUI & UI Frameworks
WWDC21 · 23 min · Swift / SwiftUI & UI Frameworks
Discover concurrency in SwiftUI
Discover how you can use Swift’s concurrency features to build even better SwiftUI apps. We’ll show you how concurrent workflows interact with your ObservableObjects, and explore how you can use them directly in your SwiftUI views and models. Find out how to use await to make your app run smoothly on the SwiftUI runloop, and learn how to fetch remote images quickly with the AsyncImage API. And we’ll take you through the process of enabling additional asynchronous flows in your custom views.
Watch at developer.apple.com ↗Code shown on screen · 8 snippets
SpacePhoto
/// A SpacePhoto contains information about a single day's photo record
/// including its date, a title, description, etc.
struct SpacePhoto {
/// The title of the astronomical photo.
var title: String
/// A description of the astronomical photo.
var description: String
/// The date the given entry was added to the catalog.
var date: Date
/// A link to the image contained within the entry.
var url: URL
}
extension SpacePhoto: Codable {
enum CodingKeys: String, CodingKey {
case title
case description = "explanation"
case date
case url
}
init(data: Data) throws {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy =
.formatted(SpacePhoto.dateFormatter)
self = try JSONDecoder()
.decode(SpacePhoto.self, from: data)
}
}
extension SpacePhoto: Identifiable {
var id: Date { date }
}
extension SpacePhoto {
static let urlTemplate = "https://example.com/photos"
static let dateFormat = "yyyy-MM-dd"
static var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = Self.dateFormat
return formatter
}
static func requestFor(date: Date) -> URL {
let dateString = SpacePhoto.dateFormatter.string(from: date)
return URL(string: "\(SpacePhoto.urlTemplate)&date=\(dateString)")!
}
private static func parseDate(
fromContainer container: KeyedDecodingContainer<CodingKeys>
) throws -> Date {
let dateString = try container.decode(String.self, forKey: .date)
guard let result = dateFormatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(
forKey: .date,
in: container,
debugDescription: "Invalid date format")
}
return result
}
private var dateString: String {
Self.dateFormatter.string(from: date)
}
}
extension SpacePhoto {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
description = try container.decode(String.self, forKey: .description)
date = try Self.parseDate(fromContainer: container)
url = try container.decode(URL.self, forKey: .url)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(description, forKey: .description)
try container.encode(dateString, forKey: .date)
}
} Photos
/// The current collection of space photos.
class Photos: ObservableObject {
private(set) var items: [SpacePhoto] = []
/// Updates `items` to a new, random list of photos.
func updateItems() async {
let fetched = fetchPhotos()
items = fetched
}
/// Fetches a new, random list of photos.
func fetchPhotos() -> [SpacePhoto] {
let downloaded: [SpacePhoto] = []
for _ in randomPhotoDates() {
}
return downloaded
}
} CatalogView
struct CatalogView: View {
private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
}
}
} Make fetch happen
/// An observable object representing a random list of space photos.
class Photos: ObservableObject {
private(set) var items: [SpacePhoto] = []
/// Updates `items` to a new, random list of `SpacePhoto`.
func updateItems() async {
let fetched = await fetchPhotos()
items = fetched
}
/// Fetches a new, random list of `SpacePhoto`.
func fetchPhotos() async -> [SpacePhoto] {
var downloaded: [SpacePhoto] = []
for date in randomPhotoDates() {
let url = SpacePhoto.requestFor(date: date)
if let photo = await fetchPhoto(from: url) {
downloaded.append(photo)
}
}
return downloaded
}
/// Fetches a `SpacePhoto` from the given `URL`.
func fetchPhoto(from url: URL) async -> SpacePhoto? {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return try SpacePhoto(data: data)
} catch {
return nil
}
}
} CatalogView
struct CatalogView: View {
private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
.refreshable {
await photos.updateItems()
}
}
.task {
await photos.updateItems()
}
}
} PhotoView with image
struct PhotoView: View {
var photo: SpacePhoto
var body: some View {
ZStack(alignment: .bottom) {
AsyncImage(url: photo.url) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(minWidth: 0, minHeight: 400)
HStack {
Text(photo.title)
Spacer()
SavePhotoButton(photo: photo)
}
.padding()
.background(.thinMaterial)
}
.background(.thickMaterial)
.mask(RoundedRectangle(cornerRadius: 16))
.padding(.bottom, 8)
}
} SavePhotoButton
struct SavePhotoButton: View {
var photo: SpacePhoto
private var isSaving = false
var body: some View {
Button {
Task {
isSaving = true
await photo.save()
isSaving = false
}
} label: {
Text("Save")
.opacity(isSaving ? 0 : 1)
.overlay {
if isSaving {
ProgressView()
}
}
}
.disabled(isSaving)
.buttonStyle(.bordered)
}
} CatalogView
struct CatalogView: View {
private var photos = Photos()
var body: some View {
NavigationView {
List {
ForEach(photos.items) { item in
PhotoView(photo: item)
.listRowSeparator(.hidden)
}
}
.navigationTitle("Catalog")
.listStyle(.plain)
.refreshable {
await photos.updateItems()
}
}
.task {
await photos.updateItems()
}
}
} Resources
Related sessions
-
13 min -
29 min -
40 min -
40 min -
34 min -
29 min -
28 min -
18 min -
36 min