Concurrency in modern Swift with Async & Await in SwiftUI — Part 6
In this article, we will continue our exploration of concurrency in modern Swift by building a simple version of an infinite scrolling list.
Here is what our final product will look like.
Let’s get started.
We will start by updating our data structure struct Coffee to conform to Equatable protocol.
Equatable types can be compared for value equality.
struct Coffee: Codable, Identifiable, Equatable {
let id: Int
let uid, blendName, origin, variety: String
let notes, intensifier: String
enum CodingKeys: String, CodingKey {
case id, uid
case blendName = "blend_name"
case origin, variety, notes, intensifier
}
}
We will update our service to turn into singleton so we don’t have to create multiple instances of the struct.
class WebService {
private init() {}
static let shared = WebService()
func getCoffeeList() async throws -> [Coffee] {
let (data, _) = try await URLSession
.shared
.data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
return try JSONDecoder().decode([Coffee].self, from: data)
}
}
We will attach task modifier to each row. We will compare each row’s coffee instance to the last instance of coffee object in array, if they are equal, we will call the web service for next 10 items.
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
.task {
if coffee == coffees.last {
do {
coffees.append(contentsOf: try await WebService.shared.getCoffeeList())
} catch {
print(error.localizedDescription)
}
}
}
}
}
}
.task {
await refreshData()
}
.navigationTitle("DevTechie")
}
}
private func refreshData() async {
do {
coffees = try await WebService.shared.getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
}
The complete code should look like this:
struct Coffee: Codable, Identifiable, Equatable {
let id: Int
let uid, blendName, origin, variety: String
let notes, intensifier: String
enum CodingKeys: String, CodingKey {
case id, uid
case blendName = "blend_name"
case origin, variety, notes, intensifier
}
}
class WebService {
private init() {}
static let shared = WebService()
func getCoffeeList() async throws -> [Coffee] {
let (data, _) = try await URLSession
.shared
.data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
return try JSONDecoder().decode([Coffee].self, from: data)
}
}
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
.task {
if coffee == coffees.last {
do {
coffees.append(contentsOf: try await WebService.shared.getCoffeeList())
} catch {
print(error.localizedDescription)
}
}
}
}
}
}
.task {
await refreshData()
}
.navigationTitle("DevTechie")
}
}
private func refreshData() async {
do {
coffees = try await WebService.shared.getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
}
Concurrency in modern Swift with Async & Await in SwiftUI — Part 5
In this part, we will learn to convert completion blocks from old APIs into newer async await versions.
Converting completion blocks into async await
Async and await were introduced in Swift 5.5 and before that, all of us were juggling between completion blocks, so it’s not a surprise that we have completion block code sitting in large quantities. With the new API calling completion block code inside async await, can be a bit challenging, so let’s try to build a solution for that too.
We will start by adding a completion block based API call to our web service.
class WebService {
func getCoffeeList() async throws -> [Coffee] {
let (data, _) = try await URLSession
.shared
.data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
return try JSONDecoder().decode([Coffee].self, from: data)
}
func getCoffeeOldWay(completion: @escaping (Result<[Coffee], Error>) -> Void) {
URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)) { data, response, error in
guard error == nil else {
completion(.failure(NSError(domain: "Error: \(error!.localizedDescription)", code: 232)))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No Data", code: 233)))
return
}
do {
let coffees = try JSONDecoder().decode([Coffee].self, from: data)
completion(.success(coffees))
} catch {
completion(.failure(NSError(domain: "Error: \(error.localizedDescription)", code: 234)))
}
}
.resume()
}
}
We will update our SwiftUI view to use this new function instead of the async await version.
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
}
}
}
.onAppear {
WebService().getCoffeeOldWay { result in
switch result {
case .success(let responseCoffees):
coffees = responseCoffees
case .failure(let err):
print(err.localizedDescription)
}
}
}
.navigationTitle("DevTechie")
}
}
private func refreshData() async {
do {
coffees = try await WebService().getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
}
Build and run.
Now when we have a completion block based function, we can bring it into the modern world.
We will use the withCheckedThrowingContinuation function for this task. withCheckedThrowingContinuation was introduced in iOS 13 and it is used to suspend the current task and then it calls the passed callback with a CheckedContinuation object.
Inside the callback, we call the completion block based function, and when it finishes, we resume the execution of the task via the CheckedContinuation that withCheckedContinuation provided.
An important note here is that we must call continuation.resume() exactly once in the withCheckedContinuation block. If we forgot to do it, our app would be blocked forever. If we do it twice, the app will crash.
Let’s add another function to our service, which will call the completion block based method and encapsulate its response in an async await manner.
class WebService {
func getCoffeeList() async throws -> [Coffee] {
let (data, _) = try await URLSession
.shared
.data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
return try JSONDecoder().decode([Coffee].self, from: data)
}
func getCoffeeOldWay(completion: @escaping (Result<[Coffee], Error>) -> Void) {
URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)) { data, response, error in
guard error == nil else {
completion(.failure(NSError(domain: "Error: \(error!.localizedDescription)", code: 232)))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No Data", code: 233)))
return
}
do {
let coffees = try JSONDecoder().decode([Coffee].self, from: data)
completion(.success(coffees))
} catch {
completion(.failure(NSError(domain: "Error: \(error.localizedDescription)", code: 234)))
}
}
.resume()
}
func getCoffeeNewishWay() async throws -> [Coffee] {
try await withCheckedThrowingContinuation({ continuation in
getCoffeeOldWay { result in
switch result {
case .success(let coffeeResponse):
continuation.resume(returning: coffeeResponse)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}
}
Update the View with new code.
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
}
}
}
.task {
do {
coffees = try await WebService().getCoffeeNewishWay()
} catch {
print(error)
}
}
.navigationTitle("DevTechie")
}
}
}
Concurrency in modern Swift with Async & Await in SwiftUI — Part 4
Let’s continue our exploration with AsyncImage and refreshable in this article.
AsyncImage
Downloading images is a common task, so the SwiftUI team decided to make things even more convenient for app developers by introducing the AsyncImage view with the release of iOS 15.
This view uses the shared instance of URLSession, similar to what we have been doing using async and await, to load an image from the specified URL.
The syntax for AsyncImage is very simple.
AsyncImage(url: URL(string: "..."))
Let’s change our code to use AsyncImage. We will continue to ask for 4000x4000 image from API and you will see the reason.
struct DevTechieAsyncAwaitExample: View {
var body: some View {
NavigationStack {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/4000"))
.frame(width: 400, height: 400)
}
.navigationTitle("DevTechie")
}
}
}
Build and run
Notice the gray box right before the image is loaded; it’s the placeholder view. AsyncImage provides a placeholder view while the system gets the image from the API. Placeholder view is replaced with the actual image once its received.
Another thing to notice in this example is the size of the image. Since we are requesting a 4000x4000 pixel image, we are getting one, but despite specifying the frame for the view, our image takes the entire screen.
The reason is that AsyncImage returns the image in its full resolution and we can’t apply manipulating modifiers on the image as they are defined at the view level.
To give more control over how and what needs to be shown inside the view, AsyncImage has another overload which gives us access to the image in trailing closure. At the same time, it also gives us the ability to define our own placeholder view.
AsyncImage(url: URL(string: "...")) { image in
image.resizable()
} placeholder: {
// a view for placeholder
}
Let’s use this overload and update our code. We will use SwiftUI’s ProgressView for the placeholder view.
struct DevTechieAsyncAwaitExample: View {
var body: some View {
NavigationStack {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/4000")) { image in
image.resizable()
.frame(width: 400, height: 400)
} placeholder: {
ProgressView()
}
.frame(width: 400, height: 400)
}
.navigationTitle("DevTechie")
}
}
}
Build and run
Async Data Refresh
Let’s continue our exploration of async await with another example. This time, we will build an app to fetch a random list of coffees. We will display them in a List view and add pull-to-refresh functionality to refresh the list with new data.
We will be using random-data-api.com to fetch dummy data, but if you have a service or API of your choice, please feel free to use that.
API url:
https://random-data-api.com/api/coffee/random_coffee?size=10
Opening the link will give us a JSON response, for better readability. Let’s change the size to be 1.
[
{
"id":1272,
"uid":"c36bd635-ec6a-43c2-8a54-b57a2c86397d",
"blend_name":"Good-morning Symphony",
"origin":"Managua, Nicaragua",
"variety":"Java",
"notes":"structured, tea-like, white pepper, tomato, lemonade",
"intensifier":"juicy"
}
]
Next, we will construct our data structure based on this JSON response.
struct Coffee: Codable, Identifiable {
let id: Int
let uid, blendName, origin, variety: String
let notes, intensifier: String
enum CodingKeys: String, CodingKey {
case id, uid
case blendName = "blend_name"
case origin, variety, notes, intensifier
}
}
Let’s add the WebService class and call the API.
class WebService {
func getCoffeeList() async throws -> [Coffee] {
let (data, _) = try await URLSession
.shared
.data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
return try JSONDecoder().decode([Coffee].self, from: data)
}
}
We will use a simple List view to render the response on UI.
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
}
}
.task {
do {
coffees = try await WebService().getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
.navigationTitle("DevTechie")
}
}
}
Build and run.
Notice that the server takes a bit to respond back, but our UI stays responsive.
We will add an empty state for the list.
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
}
}
}
.task {
do {
coffees = try await WebService().getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
.navigationTitle("DevTechie")
}
}
}
Adding pull-to-refresh is as easy as adding another modifier in SwiftUI. With the introduction of refreshable (action:), pull-to-refresh is piece of cake. We apply this modifier to a view to set the refresh value in the view’s environment to a RefreshAction instance that uses the specified action as its handler.
Refreshable modifier, just like task modifier can call async functions.
We will attach this modifier to our List view. Before we attach the refreshable modifier, let’s move our code, which calls the API, into a private function so we can reuse it.
Since we are awaiting on response, we will mark this new function as async as well.
private func refreshData() async {
do {
coffees = try await WebService().getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
Time to add pull-to-refresh
struct DevTechieAsyncAwaitExample: View {
@State private var coffees = [Coffee]()
var body: some View {
NavigationStack {
VStack {
if coffees.isEmpty {
ZStack {
Image(systemName: "heater.vertical.fill")
.font(.system(size: 60))
.rotationEffect(.degrees(-90))
.offset(y: -20)
.foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 100))
.foregroundStyle(.gray.shadow(.inner(radius: 2)))
}
} else {
List(coffees) { coffee in
VStack(alignment: .leading, spacing: 5) {
Text(coffee.blendName)
.font(.title3)
Text("Notes: \(coffee.notes)")
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Text("Origin: \(coffee.origin)")
Spacer()
Text("Variety: \(coffee.variety)")
}
.font(.caption)
.foregroundStyle(.tertiary)
}
}
.refreshable {
await refreshData()
}
}
}
.task {
await refreshData()
}
.navigationTitle("DevTechie")
}
}
private func refreshData() async {
do {
coffees = try await WebService().getCoffeeList()
} catch {
print(error.localizedDescription)
}
}
}
Build and run to see this in action.
Concurrency in modern Swift with Async & Await in SwiftUI — Part 3
We will continue our exploration of modern concurrency with task structure & task modifier and async await version of URLSession in this article.
Task Modifier
Apple introduced the task modifier with the release of iOS 15. Task modifier adds an asynchronous task to perform before the view appears.
We can use this modifier to perform any asynchronous task with the benefit that if the view’s life ends before the task is finished executing, SwiftUI will cancel the task. Unlike onAppear modifier which will keep on executing the task until its finished.
Let’s update our code to render a random image from our service, when the view appears.
struct DevTechieAsyncAwaitExample: View {
@State private var counter = 0
@State private var response: Image?
var body: some View {
NavigationStack {
VStack {
if let image = response {
image
.resizable()
.frame(width: 400, height: 400)
}
}
.task {
response = try? await WebService().getDataFromServer()
}
.navigationTitle("DevTechie")
}
}
}
Async URLSession
Since the introduction of async and await, URLSession has also added a variation so we don’t have to deal with completion blocks or trailing closures anymore.
Before async and await, URLSession had the following interface.
URLSession
.shared
.dataTask(with: req) { data, response, error in
// execute something big
}
With async and await, the API has been changed to be more readable.
let (data, _) = try await URLSession.shared.data(from: url)
Lets change our image downloading service to use URLSession instead of using overload from Data class.
class WebService {
func getDataFromServer() async throws -> Image? {
let (imageData, _) = try await URLSession
.shared
.data(from: URL(string: "https://picsum.photos/4000")!)
return Image(uiImage: UIImage(data: imageData)!)
}
}
No changes in the view, but here is the code for convenience.
struct DevTechieAsyncAwaitExample: View {
@State private var counter = 0
@State private var response: Image?
var body: some View {
NavigationStack {
VStack {
if let image = response {
image
.resizable()
.frame(width: 400, height: 400)
}
}
.task {
response = try? await WebService().getDataFromServer()
}
.navigationTitle("DevTechie")
}
}
}
Build and run
OpenURLAction in SwiftUI
OpenURL environment variable provides ability to open URLs from SwiftUI views. We can use it to open website, email form, deep-link embedded in the app or settings app on the device.
The OpenURL structure is defined in app’s Environment and it gives us access to its instance for app’s current Environment via environment variable. We use it to open a URL in response to some action performed by the user or the app.
Let’s look at an example by opening DevTechie.com via openURL
import SwiftUI
struct OpenURLExample: View {
@Environment(\.openURL) var openURL
var body: some View {
Button(action: openDevTechie) {
Text("Checkout DevTechie.com")
Image(systemName: "globe.americas.fill")
}
}
func openDevTechie() {
guard let url = URL(string: "https://www.devtechie.com") else {
return
}
openURL(url)
}
}
We can open settings app using openURL
import SwiftUI
struct OpenURLExample: View {
@Environment(\.openURL) var openURL
var body: some View {
Button(action: openDevTechie) {
Text("Checkout DevTechie.com")
Image(systemName: "globe.americas.fill")
}
Button(action: openSettingsApp) {
Text("Open Settings App")
Image(systemName: "gear.circle.fill")
}
}
func openDevTechie() {
guard let url = URL(string: "https://www.devtechie.com") else {
return
}
openURL(url)
}
func openSettingsApp() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
openURL(url)
}
}
OpenURL also supports trailing completion closure giving us access to a Boolean variable which indicates whether the URL can be opened. We can use this method when attempting to asynchronously open a URL. The result indicates whether the system was able open the URL. The completion runs after the system decides that it can open the URL.
Let’s add a function in our example. In this case, we will try to open mail url on simulator, since simulator doesn’t have mail app installed the opening of URL will fail and we will get message in console.
func openMailApp() {
guard let url = URL(string: "devtechieInc@gmail.com") else {
return
}
openURL(url) { result in
print("Opening mail app success: \(result)")
}
}
Complete code:
import SwiftUI
struct OpenURLExample: View {
@Environment(\.openURL) var openURL
var body: some View {
Button(action: openDevTechie) {
Text("Checkout DevTechie.com")
Image(systemName: "globe.americas.fill")
}
Button(action: openSettingsApp) {
Text("Open Settings App")
Image(systemName: "gear.circle.fill")
}
Button(action: openMailApp) {
Text("Send email to DevTechie")
Image(systemName: "envelope.open")
}
}
func openDevTechie() {
guard let url = URL(string: "https://www.devtechie.com") else {
return
}
openURL(url)
}
func openSettingsApp() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
openURL(url)
}
func openMailApp() {
guard let url = URL(string: "devtechieInc@gmail.com") else {
return
}
openURL(url) { result in
print("Opening mail app success: \(result)")
}
}
}
Console output:
Let’s update our code to include a failure message on screen.
import SwiftUI
struct OpenURLExample: View {
@Environment(\.openURL) var openURL
@State private var message = ""
var body: some View {
Button(action: openDevTechie) {
Text("Checkout DevTechie.com")
Image(systemName: "globe.americas.fill")
}
Button(action: openSettingsApp) {
Text("Open Settings App")
Image(systemName: "gear.circle.fill")
}
Button(action: openMailApp) {
Text("Send email to DevTechie")
Image(systemName: "envelope.open")
}
if !message.isEmpty {
Text(message)
.font(.body)
.foregroundStyle(Color.pink.gradient)
}
}
func openDevTechie() {
guard let url = URL(string: "https://www.devtechie.com") else {
return
}
openURL(url)
}
func openSettingsApp() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
openURL(url)
}
func openMailApp() {
guard let url = URL(string: "devtechieInc@gmail.com") else {
return
}
openURL(url) { result in
if !result {
message = "Failed to open the URL."
}
}
}
}
We will continue our exploration of modern concurrency with async and await.
Async and Await
The main idea behind async await is to offer support for asynchronous task execution without blocking the UI for the user. For example, if we have an app that displays an image from the internet and also has a counter, a user can tap on it to increase the counter while waiting for the app. If we make the call to download image from internet on main thread, the counter will not work as the thread will be waiting on the download to finish.
Let’s see this in code. We will create a fake web service which will wait for 5 seconds (pretending to download something) and then will send a response back to the caller.
Thread.sleep, sleeps the current thread for the given time interval, so no run loop processing occurs while the thread is blocked or sleeping.
class WebService {
func getDataFromServer() -> String {
Thread.sleep(until: Date().addingTimeInterval(5))
return "Here are the results"
}
}
Let’s use this service inside a SwiftUI view.
struct DevTechieAsyncAwaitExample: View {
@State private var counter = 0
@State private var response = ""
var body: some View {
NavigationStack {
VStack {
Text("Server said: \(response)")
Text("Tap counter: **\(counter)**")
Button("Tap me while waiting") {
counter += 1
}
.buttonStyle(.borderedProminent)
Button("Initiate task") {
response = WebService().getDataFromServer()
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("DevTechie")
}
}
}
Build and run
Notice the counter button works until we tap on the initiate task button. Reason UI is unresponsive because we are doing work on the main thread and when we call Thread.Sleep on it, the entire thread is blocked.
In a production environment, we would not be calling Thread.sleep but would be waiting for some image or content to be downloaded, but again, we don’t want to block the main thread in that case either.
Let’s see this with another example where we will download an image from a remote API. We will change our service to download 4000x4000 image from picsum.photos API.
class WebService {
func getDataFromServer() -> Image? {
let imageData = try? Data(contentsOf: URL(string: "https://picsum.photos/4000")!)
return Image(uiImage: UIImage(data: imageData!)!)
}
}
Let’s also change the view to render the downloaded image.
struct DevTechieAsyncAwaitExample: View {
@State private var counter = 0
@State private var response: Image?
var body: some View {
NavigationStack {
VStack {
if let image = response {
image
.resizable()
.frame(width: 400, height: 400)
}
Text("Tap counter: **\(counter)**")
Button("Tap me while waiting") {
counter += 1
}
.buttonStyle(.borderedProminent)
Button("Initiate task") {
response = WebService().getDataFromServer()
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("DevTechie")
}
}
}
Notice that tapping on the initiate task button, blocks the whole UI and app becomes unresponsive until the image is rendered on to the screen.
This time, even Xcode complains with a purple warning message
Here is how our app’s thread looks like while everything is being worked on the main thread.
In the past, we moved computationally heavy work in the background using GCD (which was originally written for Objective-C and was ported over to Swift). Which is still relevant, and in use, but since Swift 5.5 release, we have a Swifty way of handling the same work.
With async and await, we want to move the image download task to a background thread and when the image is downloaded, render it on main thread.
We will turn our image downloading function into an asynchronous function. Note that we are also throwing any error the service may encounter to catch it at the caller level.
class WebService {
func getDataFromServer() async throws -> Image? {
let imageData = try Data(contentsOf: URL(string: "https://picsum.photos/4000")!)
return Image(uiImage: UIImage(data: imageData)!)
}
}
Since the service is asynchronous, we can’t really call it directly from the button action as we were doing it before. We will get a compiler error.
This can be easily fixed by wrapping the call to service inside a Task structure which is designed to handle asynchronous work.
Task {
response = try? await WebService().getDataFromServer()
}
The complete view code should look like this:
struct DevTechieAsyncAwaitExample: View {
@State private var counter = 0
@State private var response: Image?
var body: some View {
NavigationStack {
VStack {
if let image = response {
image
.resizable()
.frame(width: 400, height: 400)
}
Text("Tap counter: **\(counter)**")
Button("Tap me while waiting") {
counter += 1
}
.buttonStyle(.borderedProminent)
Button("Initiate task") {
Task {
response = try? await WebService().getDataFromServer()
}
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("DevTechie")
}
}
}
Build and run.
Notice that we never stopped tapping on the button and the counter kept going up without any interruptions.
Masking views using Mask Modifier in SwiftUI
SwiftUI’s mask modifier is used for masking one view with another view. We use mask modifier when we want to apply the alpha (opacity) value of another view to the current view.
DevTechie
SwiftUI, iOS Development, iOS, Swift, DevTechie, Image View, SF Symbols, ios 15. iOS 16, iPadOS, watchOS, swiftui listwww.devtechie.com
Application of mask modifier is simple, let’s set Text view’s foreground color in gradient style with the help of mask modifier.
struct MaskModifierExample: View {
var body: some View {
LinearGradient(colors: [.pink, .blue], startPoint: .leading, endPoint: .trailing)
.mask {
Text("DevTechie.com")
.font(.system(size: 48))
}
}
}
We can use mask modifier to combine two images and create a new image.
struct MaskModifierExample: View {
var body: some View {
Image(systemName: "envelope.badge.fill")
.font(.system(size: 128, weight: .regular))
.mask {
Image(systemName: "person.fill")
.font(.system(size: 64))
}
.overlay {
Circle()
.stroke(lineWidth: 4)
}
.foregroundColor(Color.blue)
}
}
We can use mask modifier to add dim effect on images.
struct MaskModifierExample: View {
@State private var applyDim = false
var body: some View {
Image(systemName: "person.fill")
.font(.system(size: 128, weight: .regular))
.foregroundColor(.blue)
.mask({
Rectangle().foregroundColor(.orange.opacity(applyDim ? 0.2 : 0.8))
})
.onTapGesture {
applyDim.toggle()
}
}
}
Mask modifier can be used to color SF symbol based images as well.
struct MaskModifierExample: View {
@State private var gradientEndPoint: UnitPoint = .trailing
let unitPoints: [UnitPoint] = [UnitPoint.leading, .trailing, .bottom, .bottomLeading, .bottomTrailing, .center, .top, .topLeading, .topTrailing]
var body: some View {
LinearGradient(colors: [.red, .blue], startPoint: .leading, endPoint: gradientEndPoint)
.frame(width: 128, height: 128)
.mask {
Image(systemName: "flag.checkered")
.font(.system(size: 128))
}
.onTapGesture {
withAnimation {
gradientEndPoint = unitPoints.randomElement() ?? .topTrailing
}
}
}
}
Concurrency in modern Swift with Async & Await in SwiftUI — Part 1
Apple introduced async and await keywords with the release of Swift 5.5 to modernize the concurrency APIs for Apple ecosystems.
Before Swift 5.5, Grand Central Dispatch (GCD), Operation Queues and Completion blocks were the preferred way to manage and create a sequence of concurrent code. With the introduction of async and await, we don’t have to deal with unmanagable nested completion blocks anymore.
Async and await make code much more readable. The idea is that an asynchronous function must be decorated with the async keyword and the calling function must add the await before calling the asynchronous function.
SwiftUI supports calling asynchronous functions out of the box with Task structure and task modifier.
In this article, we will explore async and await.
Code Execution
By default code executes line after line unless specified by the developer to distribute the work on different threads, and we can see this with an easy example.
struct DevTechieAsyncAwaitExample: View {
var body: some View {
NavigationStack {
VStack {
Text("Code")
}
.onAppear {
print("Let's work on something")
print((1...2000).reduce(0, +))
print("Work is done")
}
.navigationTitle("DevTechie")
}
}
}
Code written in the onAppear block is called sequentially, so running it will print the following in the console.
Let's work on something
2001000
Work is done
At this point we are running our code in the main thread. This works fine as long as the code executes fast enough that the user never notices a lag or unresponsive UI; but what if there is a task which takes minutes to run? In that case the UI will be frozen, and if the UI is frozen for a long time, its not only a bad user experience, but the system may decide to kill the unresponsive app.
So the best practice is to have long-running tasks run asynchronously.
We can use GCD to fix the issue.
struct DevTechieAsyncAwaitExample: View {
var body: some View {
NavigationStack {
VStack {
Text("Code")
}
.onAppear {
print("Before GCD Block")
DispatchQueue.global().async {
print("Let's work on something")
print((1...2000).reduce(0, +))
print("Work is done")
}
print("After GCD Block")
}
.navigationTitle("DevTechie")
}
}
}
We added two additional print statements, one before the GCD block and another one right after. These print statements will tell us if we are truly running an async piece of code or not.
Results from console.
Before GCD Block
After GCD Block
Let's work on something
2001000
Work is done
Notice that the newly added print statements appear first in the console and print statements from the GCD block are printed a few moments later. This proves that our code inside the GCD is running asynchronously.
We can still continue to use the GCD, but starting iOS 15, we have a Swifty solution for doing the same with the struct called Task and its corresponding modifier task.
Let’s replace the GCD block with Task block next.
struct DevTechieAsyncAwaitExample: View {
var body: some View {
NavigationStack {
VStack {
Text("Code")
}
.onAppear {
print("Before Task Block")
Task {
print("Let's work on something")
print((1...2000).reduce(0, +))
print("Work is done")
}
print("After Task Block")
}
.navigationTitle("DevTechie")
}
}
}
Our program will still run asynchronously.
Console results.
Before Task Block
After Task Block
Let's work on something
2001000
Work is done
This gives you a good picture of the type of work we are after. More in-depth discussions about Tasks will come later in the article, but for now, let's shift our focus to async and await in the next article.
Finding Related Words using Natural Language Processing in SwiftUI
Another cool feature that Natural Language framework provides out of the box is the ability to find related or similar words.
NLEmbedding class was introduced in iOS 13 and it creates a map of string to vectors which are used to locate neighboring strings. It uses vector based proximity to find similar strings.
The vocabulary is the entire set of strings in an embedding. Each string in the vocabulary has a vector, which is an array of doubles, and each double corresponds to a dimension in the embedding. An
NLEmbedding
uses these vectors to determine the distance between two strings, or to find the nearest neighbors of a string in the vocabulary. The higher the similarity of any two strings, the smaller the distance is between them.
The API is very simple, we just create a request to find word embedding for given language and ask the system to generate neighboring strings.
Let’s start with a basic view.
import NaturalLanguage
struct DevTechieNLEmbeddingExample: View {
@State private var inputString = ""
@State private var results = [String]()
var body: some View {
NavigationStack {
VStack {
List(results, id: \.self) {item in
Text(item)
}
HStack {
TextField("Type a word", text: $inputString)
.textFieldStyle(.roundedBorder)
Button {
} label: {
Image(systemName: "paperplane.fill")
}
}
}
.padding()
.navigationTitle("DevTechie")
}
}
}
We will use TextField to type string and button will help us process and find the similar words.
Let’s create NLEmbedding instance for english language
let embedding = NLEmbedding.wordEmbedding(for: .english)!
We will use NLEmbedding’s neighbors function to get five similar words. This method returns word and its distance from the input string.
We will lowercase the input so distance vector can be computed.
let neighbors = embedding.neighbors(for: inputString.lowercased(), maximumCount: 5)
Complete example will look like this
import NaturalLanguage
struct DevTechieNLEmbeddingExample: View {
@State private var inputString = ""
@State private var results = [String]()
let embedding = NLEmbedding.wordEmbedding(for: .english)!
var body: some View {
NavigationStack {
VStack {
List(results, id: \.self) {item in
Text(item)
}
HStack {
TextField("Type a word", text: $inputString)
.textFieldStyle(.roundedBorder)
Button {
results = []
let neighbors = embedding.neighbors(for: inputString.lowercased(), maximumCount: 5)
for neighbor in neighbors {
results.append("Word: \(neighbor.0), Distance: \(neighbor.1.formatted())")
}
} label: {
Image(systemName: "paperplane.fill")
}
}
}
.padding()
.navigationTitle("DevTechie")
}
}
}
Chart Symbol Shape in Charts Framework & SwiftUI 4
Apple’s Charts framework was introduced with iOS 16 and it’s the easiest way to add data visualization to your app.
Learn more about charts here:
Mastering Charts Framework in SwiftUI 4 & iOS 16
At WWDC 22 Apple announced brand new framework for data visualization called Charts Framework. In this course we will…www.devtechie.com
Charts framework provides many customization options and one of the options is to choose the chart symbol to represent plotted values. Let’s look at this with an example.
We will start with a simple data structure to capture daily workout routines.
struct Workout: Identifiable {
let id = UUID()
let day: String
let minutes: Int
}
We will add sample data to plot on the chart.
extension Workout {
static var sample: [Workout] {
[
.init(day: "Mon", minutes: Int.random(in: 20..<60)),
.init(day: "Tue", minutes: Int.random(in: 20..<60)),
.init(day: "Wed", minutes: Int.random(in: 20..<60)),
.init(day: "Thu", minutes: Int.random(in: 20..<60)),
.init(day: "Fri", minutes: Int.random(in: 20..<60)),
.init(day: "Sat", minutes: Int.random(in: 20..<60)),
.init(day: "Sun", minutes: Int.random(in: 20..<60)),
]
}
}
Next, we will add a chart view. We will plot line charts combined with point charts to plot daily workouts.
import Charts
struct ChartSymbolExample: View {
var body: some View {
NavigationStack {
Chart(Workout.sample) { workout in
LineMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.interpolationMethod(.catmullRom)
PointMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.annotation {
Text("\(workout.minutes)")
}
}
.frame(height: 400)
.navigationTitle("DevTechie")
}
}
}
The circle shape added below the minutes is the chart symbol shape.
Charts add these chart symbol shapes if not specified, but we can customize them with .symbol(symbol: ChartSymbolShape) modifier.
There are predefined values under ChartSymbolShape and we can choose from the list. So if we want to change the symbol to be a diamond, we can change it as shown below:
PointMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.symbol(.diamond)
Complete code:
import Charts
struct ChartSymbolExample: View {
var body: some View {
NavigationStack {
Chart(Workout.sample) { workout in
LineMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.interpolationMethod(.catmullRom)
PointMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.symbol(.diamond)
.annotation {
Text("\(workout.minutes)")
}
}
.frame(height: 400)
.navigationTitle("DevTechie")
}
}
}
Other options can be found under the ChartShapeSymbol extension.
extension ChartSymbolShape where Self == BasicChartSymbolShape {
/// Circle symbol.
public static var circle: BasicChartSymbolShape { get }
/// Square symbol.
public static var square: BasicChartSymbolShape { get }
/// Triangle symbol.
public static var triangle: BasicChartSymbolShape { get }
/// Diamond symbol.
public static var diamond: BasicChartSymbolShape { get }
/// Pentagon symbol.
public static var pentagon: BasicChartSymbolShape { get }
/// Plus symbol.
public static var plus: BasicChartSymbolShape { get }
/// Cross symbol.
public static var cross: BasicChartSymbolShape { get }
/// Asterisk symbol.
public static var asterisk: BasicChartSymbolShape { get }
}
Chart Symbol Size
We can change the size of a symbol with the help of symbolSize modifier which takes CGSize parameter.
Modifier:
.symbolSize(CGSize(width: 20, height: 20))
Code:
import Charts
struct ChartSymbolExample: View {
var body: some View {
NavigationStack {
Chart(Workout.sample) { workout in
LineMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.interpolationMethod(.catmullRom)
PointMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.symbol(.diamond)
.symbolSize(CGSize(width: 20, height: 20))
.annotation {
Text("\(workout.minutes)")
}
}
.frame(height: 400)
.navigationTitle("DevTechie")
}
}
}
Chart Symbol Border
By default, symbols are drawn as solid fill, but we can change that to only draw the border of the shape by chaining .strokeBorder() modifier to the chart symbol shape.
.symbol(.asterisk.strokeBorder()):
import Charts
struct ChartSymbolExample: View {
var body: some View {
NavigationStack {
Chart(Workout.sample) { workout in
LineMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.interpolationMethod(.catmullRom)
PointMark(x: .value("Day", workout.day), y: .value("Minutes", workout.minutes))
.symbol(.diamond.strokeBorder())
.symbolSize(CGSize(width: 20, height: 20))
.annotation {
Text("\(workout.minutes)")
}
}
.frame(height: 400)
.navigationTitle("DevTechie")
}
}
}
We can change the border width by specifying the line width.
.symbol(.diamond.strokeBorder(lineWidth: 5))