Building an app that works on both iPhone and iPad isn’t particularly complicated. SwiftUI provides the necessary tools to adapt to different screen sizes so that users get the best experience for the device they are currently using.
In this tutorial we’ll look at how to build a responsive design that works on the smaller iPhone screen as well as the larger screens found on the iPad. As a bonus, we’ll use the “Mac (Designed for iPad)” setting in the target so that you can use the app on any Mac that uses Apple Silicon.
Briefly, on the Designed for iPad option for Mac: There are two other Mac destinations that you can choose from when creating your app. These are Mac Catalyst, which is a mix between Designed for iPad and a native app, and then there’s the native Mac app option. If you want the quickest way to run an app on a Mac, just check the Designed for iPad option, and it will run on any Mac that runs Apple Silicon. Rather than dig deeper into Mac Catalyst and a native Mac app, we’ll leave the Designed for iPad option selected and focus on responsive design for iPhone and iPad apps.
Add a Detail View
struct DetailView: View {
var item: String
var body: some View {
VStack {
Text(item)
.font(.largeTitle)
.bold()
Text("Detailed info about \(item)")
.padding()
}
.navigationTitle(item)
}
}
The first step on this tutorial is to add a view that I called DetailView. This is the view that users will see when selecting an item in a List view (will show that in a few moments). It accepts an item as a parameter and has a Text view with the font modifier set to .largeTitle and .bold. It also has another Text view with the default font settings for a Text view. We also set a navigationTitle to the same name.
Implementing the View
In the ContentView we make use of the horizontalSizeClass environment variable to detect what horizontal space we have available.
struct ContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State private var selectedItem: String? = nil
let items = ["Swift", "SwiftUI", "Combine", "Core Data", "UIKit"]
var body: some View {
if horizontalSizeClass == .regular {
// iPad layout (and larger iPhones in landscape)
NavigationSplitView {
List(items, id: \.self, selection: $selectedItem) { item in
Text(item)
}
.navigationTitle("Topics")
} detail: {
if let item = selectedItem {
DetailView(item: item)
} else {
Text("Select a topic")
.foregroundStyle(.secondary)
}
}
} else {
// iPhone layout
NavigationView {
List(items, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
.navigationTitle("Topics")
}
}
}
}
We begin with accessing the Environment with the @Environment property wrapper. We specify that we want access to the .horizontalSizeClass. We also call it the same. Apple stores information in the environment and by specifying what you want to access, you can work with this data wether it be the .horizontalSizeClass or a .modelContext or something else.
We use the @State property wrapper next and make a private variable called selectedItem and make it an optional String defaulted to nil. The selectedItem will be used for when the app is used on an iPad. I’ll explain this later.
On line 5 we set some items in an array that will be used to populate our List view.
On line 8 we access the horizontalSizeClass to see if its equal to .regular. Checking against .regular means that we have a wider amount of space to work with. The other option is .compact which is for iPhones and perhaps iPads in portrait.
If the horizontalSizeClass is .regular, then we work with a NavigationSplitView. This has the main side which we implement a List on line 11 using our items to populate it, along with id: \.self which is needed because items in an array and doesn’t conform to the Identifiable protocol. We also have the selectedItem which is a binding that we need to be updated to show our detail view in a moment.
On line 14, we set a title for the navigation.
Line 15 is where we implement our detail view. We check if we have selected an item, and if so, we call DetailView passing in the item that was selected. If not, we just display some generic text in the Text view.
This handles iPad in portrait and landscape mode as well as larger iPhones in landscape mode. Apple handles a lot of the work behind the scenes. You might notice that an iPad in portrait mode starts in the detail view, but has the slide out menu available at the top left, and when rotating the device, the menu stays open.
horizontalSizeClass in .compact mode
If our sizeclass on line 8 isn’t .regular, then it is .compact. When the environment has this mode available, we can use a NavigationView where we have a List view that has the items plus the id: \.self for identifying, and then a NavigationLink that sends it to the DetailView for the destination. We pass in the item here and don’t need to use the selectedItem like in the NavigationSplitView.
When running the app you will now see different layouts according to what device you have and what orientation it is in.
Although there’s a bit more to building an app than this simple tutorial, you can use the horizontalSizeClass to help guide you through which UI to use for which device.
On @Environment
The Environment contains many aspects of the device. As well as there being a horizontalSizeClass, there is also a verticalSizeClass with the same .regular and .compact options should your layout need to depend on screen height instead of width. You will also find other options that you can use to make a better application such as if accessiblity is enabled, the time zone, calendar, colour scheme, to name a few. You can also add your own Environment variables for small items that may be needed across the app.
Leave a Reply
You must be logged in to post a comment.