Creating a macOS App
You’ll start by adding a macOS target to your project, and then reusing views and data you created earlier. With a foundation in place, you’ll add some new views tailored to macOS, and modify others to work better across platforms.
Add a macOS Target to the Project
Start by adding a macOS target to the project. Xcode adds a new group and set of starter files for the macOS app, along with the scheme needed to build and run the app. You’ll then add some existing files to the new target.
To be able to preview and run the app, be sure your Mac is running macOS Monterey or later.

Step 1
Choose File > New > Target. When the template sheet appears, choose the macOS tab, select the App template, and click Next.
This template adds a new macOS app target to the project.
Step 2
In the sheet, enter Mac
as the Product Name. Set the interface to SwiftUI, the life cycle to SwiftUI App, and the language to Swift, and then click Finish.
Step 3
Set the scheme to MacLandmarks > My Mac.
By setting the scheme to My Mac, you can preview, build, and run the macOS app. As you move through the tutorial, you’ll use the other schemes to keep an eye on how other targets respond to changes in shared files.
Step 4
In the MacLandmarks group, select Content
, open the Canvas, and click Resume to see the preview.
SwiftUI provides both a default main view and its preview provider, just like for an iOS app, enabling you to preview the app’s main window.
Step 5
In the Project navigator, delete the Mac
file from the MacLandmarks group. When asked, choose Move to Trash.
Like with the watchOS app, you don’t need the default app structure because you’ll reuse the one you already have.
Next, you’ll share view, model, and resource files from the iOS app with the macOS target.
Step 6
In the Project navigator, Command-click to select the following files: Landmarks
, Landmark
, Landmark
, Circle
, Map
, and Favorite
.
The first of these is the shared app definition. The others are views that will work on macOS.
Step 7
Continue Command-clicking to select all the items in the Model and Resources folders, as well as Asset
.
These items define the app’s data model and resources.
Step 8
In the File inspector, add MacLandmarks to the Target Membership for the selected files.
Add a macOS app icon set to match those for the other targets.
Step 9
Select the Assets
file in the MacLandmarks group and delete the empty AppIcon item.
You’ll replace this in the next step.
Step 10
Drag the App
folder from the downloaded projects’ Resources
folder into the MacLandmark’s Asset catalog.
Step 11
In Content
in the MacLandmarks group, add Landmark
as the top level view, with constraints on the frame size.
The preview no longer builds because the Landmark
uses Landmark
, but you haven’t defined a detail view for the macOS app yet. You’ll take care of that in the next section.
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
LandmarkList()
.frame(minWidth: 700, minHeight: 300)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ModelData())
}
}
Create a macOS Detail View
The detail view displays information about the selected landmark. You created a view like this for the iOS app, but different platforms require different approaches to data presentation.
Sometimes you can reuse a view across platforms with small adjustments or conditional compilation, but the detail view differs enough for macOS that it’s better to create a dedicated view. You’ll copy the iOS detail view as a starting point, and then modify it to suit the larger display of macOS.

Step 1
Create a new custom view in the MacLandmarks group targeting macOS called Landmark
.
You now have three files called Landmark
. Each serves the same purpose in the view hierarchy, but provides an experience tailored to a particular platform.
Step 2
Copy the iOS detail view contents into the macOS detail view.
The preview fails because the navigation
method isn’t available in macOS.
Step 3
Delete the navigation
modifier and add a frame modifier to the preview so you can see more of the content.
The Map
remains blank unless you start the live preview.
The changes you’ll make in the next few steps improve the layout for the larger display of a Mac.
Step 4
Change the HStack
holding the park and state to a VStack
with leading alignment, and remove the Spacer
.
Step 5
Enclose everything below Map
in a VStack
, and then place the Circle
and the rest of the header in an HStack
.
Step 6
Remove the offset from the circle, and instead apply a smaller offset to the entire VStack
.
Step 7
Add a resizable()
modifier to the image, and constrain the Circle
to be a bit smaller.
Step 8
Constrain the Scroll
to a maximum width.
This improves readability when the user makes the window very wide.
Step 9
Change the Favorite
to use the plain
button style.
Using the plain style here makes the button look more like the iOS equivalent.
The larger display gives you more room for additional features.
Step 10
Add an “Open in Maps” button in a ZStack
so that it appears on top of the map in the upper-right corner.
Be sure to include MapKit to be able to create the MKMap
that you send to Maps.
LandmarkDetail.swift
import SwiftUI
import MapKit
struct LandmarkDetail: View {
var modelData: ModelData
var landmark: Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
MapView(coordinate: landmark.locationCoordinate)
.ignoresSafeArea(edges: .top)
.frame(height: 300)
Button("Open in Maps") {
let destination = MKMapItem(placemark: MKPlacemark(coordinate: landmark.locationCoordinate))
destination.name = landmark.name
destination.openInMaps()
}
.padding()
}
VStack(alignment: .leading, spacing: 20) {
HStack(spacing: 24) {
CircleImage(image: landmark.image.resizable())
.frame(width: 160, height: 160)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
.buttonStyle(.plain)
}
VStack(alignment: .leading) {
Text(landmark.park)
Text(landmark.state)
}
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Divider()
Text("About \(landmark.name)")
.font(.title2)
Text(landmark.description)
}
.padding()
.frame(maxWidth: 700)
.offset(y: -50)
}
.navigationTitle(landmark.name)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: modelData.landmarks[0])
.environmentObject(modelData)
.frame(width: 850, height: 700)
}
}

Update the Row View
The shared Landmark
view works in macOS, but it’s worth revisiting to look for improvements given the new visual environment. Because this view is used by all three platforms, you need to be careful that any changes you make work across all of them.

Before modifying the row, set up a preview of the list, because the changes you’ll make are driven by how the row looks in context.
Step 1
Open Landmark
and add a minimum width.
This improves the preview, but also ensures that the list never becomes too small as the user resizes the macOS window.
Step 2
Pin the list view preview so that you can see how the row looks in context as you make changes.
Step 3
Open Landmark
and add a corner radius to the image for a more refined look.
Step 4
Wrap the landmark name in a VStack
and add the park as secondary information.
Step 5
Add vertical padding around the contents of the row to give each row a little more breathing room.
The updates improve the look in macOS, but you also need to consider the other platforms that use the list. Consider watchOS first.
Step 6
Choose the WatchLandmarks target to see a watchOS preview of the list.
The minimum row width isn’t appropriate here. Because of this and other changes you’ll make to the list in the next section, the best solution is to create a watch-specific list that omits the width constraint.
Step 7
Add a new SwiftUI view to the WatchLandmarks Extension folder called Landmark
that targets only WatchLandmarks Extension, and remove the older file’s WatchLandmarks Extension target membership.
Step 8
Copy the contents of the old Landmark
into the new one, but without the frame modifier.
The content now has the right width, but each row has too much information.
Step 9
Go back to Landmark
and add an #if
condition to prevent the secondary text from appearing in a watchOS build.
For the row, using conditional compilation is appropriate because the differences are small.
Finally, consider how your changes work for iOS.
Step 10
Choose the Landmarks build target to see what the list looks like for iOS.
The changes work well for iOS, so there’s no need to make any updates for that platform.

Update the List View
Like Landmark
, Landmark
already works on macOS, but could use improvements. For example, you’ll move the toggle for showing only favorites to a menu in the toolbar, where it can be joined by additional filtering controls.
The changes you’ll make will work for both macOS and iOS, but will be difficult to accommodate on watchOS. Fortunately, in the previous section you already split the list into a separate file for watchOS.

Step 1
Return to the MacLandmarks scheme, and in the Landmark
file that targets iOS and macOS, add a Toolbar
containing a Menu
inside a new toolbar
modifier.
You won’t be able to see the toolbar updates until you run the app.
Step 2
Move the favorites Toggle
into the menu.
This moves the toggle into the toolbar in a platform-specific way, which has the additional benefit of making it accessible no matter how long the list of landmarks gets, or how far down the user scrolls.
With more room available, you’ll add a new control for filtering the list of landmarks by category.
Step 3
Add a Filter
enumeration to describe filter states.
Match the case strings to the Category
enumeration in the Landmark
structure so that you can compare them, and include an all
case to turn filtering off.
Step 4
Add a filter
state variable, defaulting to the all
case.
By storing the filter state in the list view, the user can open multiple list view windows, each with its own filter setting, to be able to look at the data in different ways.
Step 5
Update filtered
to take into account the new filter
setting, combined with the category of a given landmark.
Step 6
Add a Picker
to the menu to set the filter category.
Because the filter has only a few items, you use the inline
picker style to make them all appear together.
Step 7
Update the navigation title to match the state of the filter.
This change will be useful in the iOS app.
Step 8
Add a second child view to the Navigation
as a placeholder for the second view in wide layouts.
Adding the second child view automatically converts the list to use the sidebar list style.
Step 9
Run the macOS target and see how the menu operates.
Step 10
Choose the Landmarks build target, and use the live preview to see that the new filtering works well for iOS as well.

Add a Built-in Menu Command
When you create an app using the SwiftUI life cycle, the system automatically creates a menu with commonly needed items, like those for closing the front-most window or for quitting the app. SwiftUI lets you add other common commands with built-in behavior, as well as completely custom commands.
In this section, you’ll add a system-provided command that lets the user toggle the sidebar, to be able to get it back after dragging it closed.

Step 1
Return to the MacLandmarks target, run the macOS app, and drag the separator between the list and detail view all the way to the left.
When you let go of the mouse button, the list disappears with no way to get it back. You’ll add a command to fix this.
Step 2
Add a new Swift file called Landmark
and set its targets to include both macOS and iOS.
You also target iOS because the shared Landmark
will eventually depend on some of the types you define in this file.
Step 3
Import SwiftUI and add a Landmark
structure that conforms to the Commands
protocol, with a computed body property.
Like a View
structure, a Commands
structure requires a computed body property that uses builder semantics, except with commands instead of views.
Step 4
Add a Sidebar
command to the body.
This built-in command set includes the command for toggling the sidebar.
To make use of commands in an app, you have to apply them to a scene, which you’ll do next.
Step 5
Open the Landmarks
file, and apply Landmark
using the commands(content:)
scene modifier.
Scene modifiers work like view modifiers, except that you apply them to scenes instead of views.
Step 6
Run the macOS app again, and see that you can use the View > Toggle Sidebar menu command to restore the list view.
Unfortunately, the watchOS app fails to build because Commands
has no watchOS availability. You’ll fix that next.
Step 7
Add a condition around the commands modifier to omit it for the watchOS app.
The watchOS app builds again.
LandmarksApp.swift
import SwiftUI
@main
struct LandmarksApp: App {
private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
#if !os(watchOS)
.commands {
LandmarkCommands()
}
#endif
#if os(watchOS)
WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
#endif
}
}
Add a Custom Menu Command
In the previous section, you added a built-in menu command set. In this section, you’ll add a custom command for toggling the favorite status of the currently selected landmark. To know which landmark is currently selected, you’ll use a focused binding.

Step 1
In Landmark
, extend the Focused
structure with a selected
value, using a custom key called Selected
.
The pattern for defining focused values resembles the pattern for defining new Environment
values: Use a private key to read and write a custom property on the system-defined Focused
structure.
Step 2
Add a @Focused
property wrapper to track the currently selected landmark.
You’re reading the value here. You’ll set it later in the list view, where the user makes the selection.
Step 3
Add a new Command
to your commands called Landmarks.
You’ll define content for the menu next.
Step 4
Add a button to the menu that toggles the selected landmark’s favorite status, and that has an appearance that changes depending on the currently selected landmark and its state.
Step 5
Add a keyboard shortcut for the menu item with the keyboard
modifier.
SwiftUI automatically shows the keyboard shortcut in the menu.
The menu now contains your new command, but you need to set the selected
focused binding for it to work.
Step 6
In Landmark
, add a state variable for the selected landmark and a computed property that indicates the index of the selected landmark.
Step 7
Initialize the List
with a binding to the selected value, and add a tag to the navigation link.
The tag associates a particular landmark with the given item in the For
, which then drives the selection.
Step 8
Add the focused
modifier to the Navigation
, providing a binding the value from the landmarks
array.
You perform a look-up here to ensure that you are modifying the landmark stored in the model, and not a copy.
Step 9
Run the macOS app and try out the new menu item.
Add Preferences with a Settings Scene
Users expect to be able to adjust settings for a macOS app using the standard Preferences menu item. You’ll add preferences to MacLandmarks by adding a Settings
scene. The scene’s views define the contents of the preferences window, which you’ll use to control the initial zoom level of the Map
. You communicate the value to the map view, and store it persistently, by using the @App
property wrapper.

You’ll start by adding a control in the Map
that sets the initial zoom to one of three levels: near, medium, or far.
Step 1
In Map
, add a Zoom
enumeration to characterize the zoom level.
Step 2
Add an @App
property called zoom
that takes on the medium
zoom level by default.
Use a storage key that uniquely identifies the parameter like you would when storing items in User
, because that’s the underlying mechanism that SwiftUI relies on.
Step 3
Change the longitude and latitude delta used to construct the region
property to a value that depends on zoom
.
To ensure that SwiftUI refreshes the map whenever delta
changes, you’ll have to alter the way you calculate and apply the region
.
Step 4
Replace the region
state variable, the set
method, and the map’s on
modifier with a computed region
property that you pass to the Map
initializer as a constant binding.
Next, you’ll create a Settings
scene that controls the stored zoom
value.
Step 5
Create a new SwiftUI view called Landmark
that targets only the macOS app.
Step 6
Add an @App
property that uses the same key as you used in the map view.
Step 7
Add a Picker
that controls the zoom
value through a binding.
You typically use a Form
to arrange controls in your settings view.
Step 8
In Landmarks
, add the Settings
scene to your app, but only for macOS.
Step 9
Run the app and try setting the preferences.
Notice that the map refreshes whenever you change the zoom level.