How to Handle Content Unavailable Gracefully with iOS 17?

Santosh Botre
6 min readJun 27, 2023
https://tenor.com/en-GB/view/no-result-gif-19599327

We iOS developers have handled the content that is not available, no search results are found, or no matching data is available scenarios in most of our iOS applications. UI/UX designers and we as developers take various approaches to provide a seamless user experience.

Here are some common strategies:

  1. Empty State Views: Instead of displaying a blank screen, developers can design and implement empty state views that communicate the absence of content or search results.

2. Placeholder Content: In situations where dynamic content is expected but unavailable, developers can utilize placeholder content to give users a sense of what to expect.

3. Error Handling: When encountering errors or failures in retrieving data, developers can implement error-handling mechanisms to communicate the issue to users. This can involve displaying error messages with clear explanations, error codes, or suggestions for troubleshooting. By providing meaningful feedback, users can understand the problem and potentially take corrective actions.

  • Show Error As Alert:
  • Show Error In View:

Different variants for handling the same kind of scenarios to intimate/inform users.

iOS 17 has unified this behaviour by providing a new view called ContentUnavailableView.

ContentUnavailableView is an interface, consisting of a label and additional content, that we display when the content of our app is unavailable to users.

Available from?

When to use it?

Situations where a view’s content cannot be displayed.

  • A network error
  • A list without items
  • A search that returns no results etc.

You create a ContentUnavailableView in its simplest form, by providing a label and some additional content such as a description or a call to action:

What we can show in UI?

ContentUnavailableView is separated into three sections.

Create Content Unavailable View

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public struct ContentUnavailableView<Label, Description, Actions> : View where Label : View, Description : View, Actions : View {

public init(label: View, description: View, actions: View )

}

Parameters:

  • label: The label that describes the view.
  • description: The view that describes the interface.
  • actions: The content of the interface actions.
ContentUnavailableView(label: {
// View - Title Section
}, description: {
// View - Description Section
}, actions: {
// View - Action Section
})

Code to have the above ContentUnavailableView.

ContentUnavailableView(label: {
Text("Title Section")
}, description: {
Text("Description Section")
}, actions: {
Text("Action Section")
})

Let’s play with ContentUnavailableView to explore the different combinations we will end up having.

Sample 1: Title + Description

  ContentUnavailableView(label: {
Text("No Results")
}, description: {
Text("Check the spelling or try a new search")
})

Sample 2: Title Label + Description

  ContentUnavailableView {
Label("Authenticate", systemImage: "faceid")
} description: {
Text("To see the profile details... Please authenticate")
}

Sample 3: Title + Description + Actions

  ContentUnavailableView(label: {
Text("Restricted Access")
},
description: {
Text("To see the profile details... Please authenticate")
},
actions: {
Button("Login", action: {
//TODO:
})
})

Sample 4: Title + Description + Actions

  ContentUnavailableView(label: {
Label("No Result", systemImage: "magnifyingglass")
},
description: {
Text("Unable to fetch the data due to some error. please try again...")
.foregroundColor(.red)
.fontWeight(.medium)
},
actions: {
HStack {
Button("Retry", action: {
//TODO:
})
Spacer()
Button("Cancel", action: {
//TODO:
})
}
})

Many of these scenarios are common in most applications.

The system provides default ContentUnavailableViews that you can use in specific situations.

Right now there is only one in-built ContentUnavailableView but by the time it goes into public release, it will have many more…

Sample 5: The example below illustrates the usage of the search view:

 ContentUnavailableView.search
 ContentUnavailableView.search(text: "Apple Product")

ContentUnavailableView combination

Combination of all the ContentUnavailableView scenarios…

ContentUnavailableView combinations:

I have created a ViewBuilder for all the combinations


@ViewBuilder
func ProfileUnavailableView_v0() -> some View {
ContentUnavailableView(label: {
Text("Title Section")
}, description: {
Text("Description Section")
}, actions: {
Text("Action Section")
})
}

@ViewBuilder
func ProfileUnavailableView_v1() -> some View {
ContentUnavailableView(label: {
Text("No Results")
}, description: {
Text("Check the spelling or try a new search")
})
}

func ProfileUnavailableView_v2() -> some View {
ContentUnavailableView {
Label("Authenticate", systemImage: "faceid")
} description: {
Text("To see the profile details... Please authenticate")
}
}

@ViewBuilder
func ProfileUnavailableView_v3(action: @escaping (() -> ())) -> some View {
ContentUnavailableView(label: {
Text("Restricted Access")
},
description: {
Text("To see the profile details... Please authenticate")
},
actions: {
Button("Login", action: {
action()
})
})
}

@ViewBuilder
func ProfileUnavailableView_v4(retryAction: @escaping (() -> ()), cancelAction: @escaping (() -> ())) -> some View {
ContentUnavailableView(label: {
Label("No Result", systemImage: "magnifyingglass")
},
description: {
Text("Unable to fetch the data due to some error. please try again...")
.foregroundColor(.red)
.fontWeight(.medium)
},
actions: {
HStack {
Button("Retry", action: {
retryAction()
})
Spacer()
Button("Cancel", action: {
cancelAction()
})
}
})
}

@ViewBuilder
func ProfileUnavailableView_v5() -> some View {
ContentUnavailableView.search
}

#Preview {
VStack {
ProfileUnavailableView_v0()
ProfileUnavailableView_v1()
ProfileUnavailableView_v2()
ProfileUnavailableView_v3(action: {
//TODO:
})
ProfileUnavailableView_v4(retryAction: {
//TODO:
}, cancelAction: {
//TODO:
})
ProfileUnavailableView_v5()

}
}

ContentUnavailableView for search screen complete code:

File 1: HomeView.swift

import SwiftUI

struct HomeView: View {
@ObservedObject private var homeViewModel = HomeViewModel()
var body: some View {
NavigationStack {
List {
ForEach(homeViewModel.products, id: \.id) { product in
VStack {
Text(product.brand)
.padding()
}
}
}
.searchable(text: $homeViewModel.searchString)
.overlay{
if homeViewModel.products.isEmpty {
ContentUnavailableView.search
}
}
}
}
}

#Preview {
HomeView()
}

File 2: HomeViewModel.swift

import Foundation

class HomeViewModel: ObservableObject {
var productMockData : [Product]

var products : [Product] {
if searchString.isEmpty {
return productMockData
} else {
return productMockData.filter{ $0.brand.localizedCaseInsensitiveContains(searchString) }
}
}

@Published var searchString: String = ""
init() {
var products: [Product] = []
if let url = Bundle.main.url(forResource: "ProductsData", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let welcome = try decoder.decode(Welcome.self, from: data)
products = welcome.products
} catch {
print("error:\(error)")
}
}
self.productMockData = products
}
}

File 3: Product.swift

// MARK: - Welcome
struct Welcome: Codable {
let products: [Product]
let total, skip, limit: Int
}

// MARK: - Product
struct Product: Codable {
let id: Int
let title, description: String
let price: Int
let discountPercentage, rating: Double
let stock: Int
let brand, category: String
let thumbnail: String
let images: [String]
}

File 4: ProductsData.json

It is created with the help of https://dummyjson.com/products

Important Tip:

The overlay of the ContentUnavailableView only works with

  1. List
  2. NavigtationView
  3. TabView
  4. ScrollView

Remember that ContentUnavailableView is for the container view and not the control views like Text, Label, Image, VStack, HStack etc. kind of views.

--

--

Santosh Botre

Take your time to learn before develop, examine to make it better, and eventually blog your learnings.