Heads up... You’re accessing parts of this content for free, with some sections shown as
text.
Heads up... You’re accessing parts of this content for free, with some sections shown as
text.Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.
Unlock now
Now, you’ll go over the improvements you applied in this lesson.
AsyncImage
Start 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))
Starting a Task on View Loading
To 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 ?? []
}
}
Refreshing Views With Pull-to-Refresh
SwiftUI 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()
}
Using onTapGesture
Open 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)
Implementing Persistence With an Actor
First, 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)
}
}
}