Concurrency Demystified

Jun 20 2024 · Swift 5.10, iOS 17, Xcode 15.3

Lesson 03: Background Tasks Made Easy with Async/Await

App Improvements Demo

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

00:01Now, you’ll go over the improvements you applied in this lesson.

00:05Start Xcode and open the starter project in the folder 03-background-tasks-made-easy-with-async-await.

AsyncImage(url: URL(string: url)) { image in
  image
    .resizable()
    .aspectRatio(contentMode: .fit)
    .background(.clear)
    .mask(RoundedRectangle(cornerRadius: 8))
} placeholder: {
  ProgressView()
    .frame(alignment: .center)
}
.frame(maxWidth: .infinity, alignment: .center)
image
  .resizable()
  .aspectRatio(contentMode: .fit)
  .background(.clear)
  .mask(RoundedRectangle(cornerRadius: 8))

01:11To start downloading the news when the app is launched, you used the .task modifier on the view where you want the task to start. .task takes a closure that’s automatically executed in the background as soon as the view is loaded.

@State private var isLoading = false
.overlay {
  if isLoading {
    ProgressView()
  } else if shouldPresentContentUnavailable {
    ContentUnavailableView {
      Label("Latest News", systemImage: "newspaper.fill")
    }
  }
}
Button("Load Latest News") { newsViewModel.fetchLatestNews() }
.task {
  isLoading = true
  await newsViewModel.fetchLatestNews()
  isLoading = false
}
@MainActor
func fetchLatestNews() async {
  news.removeAll()
  Task {
  let news = try? await newsService.latestNews()


  self.news = news ?? []
  }
}

02:27SwiftUI natively supports the pull-to-refresh gesture. To add this feature to your app, you just need to add the .refreshable modifier to the view that you want to refresh.

.refreshable {
  await newsViewModel.fetchLatestNews()
}

03:02Open the file NewsView.swift, and make the following changes:

@Environment(\.openURL)
var openURL
.onTapGesture {
  if let url = article.url {
    openURL(url)
  }
}
var body: some View {
  VStack(alignment: .center)NavigationStack {
    List {
      ForEach(newsViewModel.news, id: \.url) { article in
        ArticleView(article: article)
          .listRowSeparator(.hidden)
          .onTapGesture {
            if let url = article.url {
              openURL(url)
            }
          }
      }
    }
    .navigationTitle("Latest Apple News")
    .listStyle(.plain)

04:04First, add the Persistence component in charge of downloading and saving the article’s image.

import OSLog

actor Persistence {
  func saveToDisk(_ article: Article) {
    guard let fileURL = fileName(for: article) else {
      Logger.main.error("Can't build filename for article: \(article.title)")
      return
    }

    guard let imageURL = article.urlToImage, let url = URL(string: imageURL) else {
      Logger.main.error("Can't build image URL for article: \(article.title)")
      return
    }

    Task.detached(priority: .background) {
      guard let (downloadedFileURL, response) = try? await URLSession.shared.download(from: url) else {
        Logger.main.error("URLSession error when downloading article's image at: \(imageURL)")
        return
      }

      guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        Logger.main.error("Response error when downloading article's image at: \(imageURL)")
        return
      }

      Logger.main.info("File downloaded to: \(downloadedFileURL.absoluteString)")

      do {
        if FileManager.default.fileExists(atPath: fileURL.path) {
          try FileManager.default.removeItem(at: fileURL)
        }
        try FileManager.default.moveItem(at: downloadedFileURL, to: fileURL)
        Logger.main.info("File saved successfully to: \(fileURL.absoluteString)")
      } catch {
        Logger.main.error("File copy failed with: \(error.localizedDescription)")
      }
    }
  }

  private func fileName(for article: Article) -> URL? {
    let fileName = article.title
    guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
      return nil
    }
    return documentsDirectory.appendingPathComponent(fileName)
  }
}
Task.detached(priority: .background) {
  ...
}
guard let (downloadedFileURL, response) = try? await URLSession.shared.download(from: url) else {
  ...
}
do {
  if FileManager.default.fileExists(atPath: fileURL.path) {
    try FileManager.default.removeItem(at: fileURL)
  }
  try FileManager.default.moveItem(at: downloadedFileURL, to: fileURL)
  Logger.main.info("File saved successfully to: \(fileURL.absoluteString)")
} catch {
  Logger.main.error("File copy failed with: \(error.localizedDescription)")
}
let persistence: Persistence

@Environment(\.openURL)
var openURL
HStack {
  Text(article.publishedAt?.formatted() ?? "Date not available")
    .font(.caption)
  Spacer()
  Button("", systemImage: "square.and.arrow.up") {
    if let url = article.url {
      openURL(url)
    }
  }
  Button("", systemImage: "square.and.arrow.down") {
    Task { await persistence.saveToDisk(article) }
  }
}
.buttonStyle(BorderlessButtonStyle())
ArticleView(article: .sample, persistence: Persistence())
private let persistence = Persistence()
ForEach(newsViewModel.news, id: \.url) { article in
  ArticleView(article: article, persistence: persistence)
    .listRowSeparator(.hidden)
    .onTapGesture {
      if let url = article.url {
        openURL(url)
      }
    }
}
See forum comments
Cinema mode Download course materials from Github
Previous: SwiftUI Background Tasks Next: Conclusion