Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

21. Converting an iOS App to macOS
Written by Sarah Reichelt

If you have worked through the early chapters of this book, you have built several iOS apps. You have used Catalyst to run an iOS app on your Mac, and you have created a multi-platform app that runs on both iOS and macOS, and in the last chapter, you made a document-based Mac app. But in this chapter, you are going to make a macOS app from an iOS app. You will use the code, views and assets from an iOS project to make your macOS app.

The vast majority of Swift and SwiftUI tutorials and examples on the internet are for iOS, mostly specifically for iPhones. So learning how to re-use the code in an iOS project to create a real Mac app, will be a very valuable skill.

Getting started

Download the starter project, which is the iOS app that you are going to convert. You may have already built this app in earlier chapters, but even if you have, please use this starter project.

Build and run the app in an iPhone simulator and click through all the options to see how it works.

iOS App
iOS App

The iOS version uses a very common navigational pattern where the initial screen offers a selection of choices which then use NavigationLinks to display other views. These secondary views sometimes have even more options, which can be full navigation views, sheets or dialogs.

For the Mac version, where you can assume much wider screens, you are going to have the main navigation in a sidebar on the left. The main portion of the window on the right will display different views depending on the navigation selections.

As you work through this chapter, there will be a lot of editing which can be hard to explain and even harder to follow, but if you get lost, download the final project and check out the code there.

Setting up the Mac app

In Xcode, create a new project, using the macOS App template and selecting SwiftUI, SwiftUI App and Swift for the Interface, Life Cycle and Language. Call the app MountainAirportMac and save it.

Importing code files

To start, switch to Finder and open the MountainAirport folder inside the starter projects folder. Then select all these folders and files, except for the folder named Assets.xcassets, and drag them into the Project navigator for your new Mac project — be sure to select Copy items if needed and Create groups for each one. Confirm that the MountainAirportMac target is checked.

After you move the files, delete MountainAirport.swift and the Base.lproj folder. These are iOS specific files that are no longer needed for your new Mac app.

By the end of that process, your Project navigator should look like this:

Project navigator
Project navigator

You now have a lot of the working code from the iOS app in your macOS app. You can assume that the model classes and structs are mostly working fine and do not need to be edited, so your main task is going to be to change the SwiftUI code to make the user interface work on a Mac. But you have already saved yourself a heap of time and trouble by importing all this code. Next, you’ll import assets.

Importing assets

As well as the .swift files, you can import the assets used by the iOS app, primarily the app icon and any images used in the app’s UI.

First, go to Assets.xcassets in your Project navigator and open the Assets.xcassets folder in the iOS project. Now, drag everything from this folder into your list of assets.

Assets
Assets

This adds all the assets but not all of them are configured correctly for a Mac project, so now you have some house-keeping to do, starting with the app icon.

In the assets list, you have AppIcon that was created with the app template and AppIcon-1 that you just imported. Unfortunately, iOS and macOS have very different image size requirements for their app icons. The best solution is to take the largest of the images from AppIcon-1 and use an icon creator utility to make all the right image sizes, but for now, you are going to cheat and take the easy way out.

In AppIcon-1, select the icon at the bottom: App Store iOS 1024pt and press Command-C to copy it. Go to AppIcon, select App Store - 2x and press Command-V to paste in the copied image. Now you can delete AppIcon-1 and your Mac app will use the imported icon.

You can delete the launch-assets group as Mac apps do not have a launch view.

Select the ascending-airplane asset, click on the image and make sure that you can see the Attributes inspector on the right.

In the Devices section, Universal is checked, meaning that this image will work on any Apple device. But the image is in the 3x box for the very high-resolution iPhones and iPads and 3x images do not work in a Mac app. Drag the image from the 3x box to the 2x box to make it Mac-compatible.

Repeat this process for all the images that are 3x, not forgetting the ones in the award-images folder.

Image assets
Image assets

Now it’s time to build!

Fixing the build errors

You have imported all the code files, imported the assets, set up your app’s icon and configured the other images for the Mac. The big task now is to get the app to build.

Press Command-B to build the app, but don’t panic when you get a string of errors appearing. This is to be expected when you import code that was written for a different device.

Open the Issue navigator to see all the errors. There are eight issues you need to fix. The first four are caused by the iOS app using features that are not available in macOS.

Replacing unavailable features

For each of the errors, find the matching error in the Issue navigator. Click on the line with the red X to jump to the line of code with the error and then follow these instructions to fix it.

  1. StackNavigationViewStyle is unavailable in macOS - AwardsView.swift:

Previews can be displayed inside NavigationViews which can be really useful on iOS for seeing how they will look with the navigation bar, but this is not necessary for macOS. Replace previews with this:

static var previews: some View {
  AwardsView()
    .environmentObject(AppEnvironment())
}
  1. InsetGroupListStyle is unavailable in macOS - SearchFlights.swift:

Search the Developer Documentation for the ListStyle protocol and check the available list styles. You can click through each and check the availability for macOS. Once you have looked at the options, change this to InsetListStyle() and save the file, which will get rid of two errors!

  1. StackNavigationViewStyle is unavailable in macOS - WelcomeView.swift:

For this app the default style will be fine, so you can delete the navigationViewStyle modifier.

  1. ActionSheet is unavailable in macOS - FlightSearchDetails.swift:

iOS has Alerts and ActionSheets but macOS only has Alerts. You can change this to use an Alert, but to get the app building as quickly as possible, comment out the actionSheet modifier completely.

The easiest way to make sure you get the entire modifier is to double-click on the opening curly brace in the .actionSheet line. This will select the modifier and all its contents so you can press Command-/ to comment out the section.

Clearing remaining errors

Now you have two or three remaining issues to get rid of, depending on how Xcode is behaving on the day.

  1. Cannot find type UIColor in scope - FlightInformation.swift:

UIColor is a UIKit color object. The equivalent in AppKit is NSColor, but you are not going to use this color in the Mac version, so delete the timelineColor computed property to get rid of this error.

  1. Value of type some View has no member navigationBarItems - FlightStatusBoard.swift:

Instead of a navigation bar item for this Toggle, you are going to use a Mac toolbar. Replace the navigationBarItems modifier with this toolbar modifier:

.toolbar {
  Toggle("Hide Past", isOn: $hidePast)
}

This uses the same Toggle control but wrapped in a toolbar instead of in navigationBar.

  1. You may still have an issue with the compiler complaining because there were other errors, but it should disappear automatically. If you press Command-B now, the app will build successfully.

Well done! You now have a Mac app project populated with a lot of code and assets from an iOS app and with no build issues!

Before you try running the app, go to ContentView.swift file and replace the standard Text('Hello, world!') with WelcomeView(). Now build and run.

Expand the window so you can see both columns. Click in the darker sections of the pane on the left and data will appear on the left. It isn’t pretty but it is working! In the next sections, you are going to make it look much better.

Run 1
Run 1

Styling the sidebar

The sidebar in the app is going to show the main navigation links to the other parts of the app. Open up WelcomeView.swift and take a look at what it is doing right now. The main action is in a NavigationView that contains a grid of NavigationLinks. This is not a scheme that performs well on macOS, so you are going to replace it with a set of buttons that set a variable to dictate what is shown in the main part of the window.

To fit the Mac window better, the navigation buttons are going to be in a column of square buttons, not a grid of tall buttons.

First, go to WelcomeButtonView.swift and change the last frame modifier to:

.frame(width: 155, height: 155, alignment: .leading)

This makes the button height the same as its width. This button view is used by all the navigation buttons in the sidebar.

Back in WelcomeView.swift, replace body with this:

var body: some View {
  // 1
  VStack {
    // 2
    Button(action: { displayState = .flightBoard }, label: {
      FlightStatusButton()
    })
    // 3
    .buttonStyle(PlainButtonStyle())

    Button(action: { displayState = .searchFlights }, label: {
      SearchFlightsButton()
    }).buttonStyle(PlainButtonStyle())

    Button(action: { displayState = .awards }, label: {
      AwardsButton()
    }).buttonStyle(PlainButtonStyle())

    if let lastFlight = lastViewedFlight {
      Button(action: {
        displayState = .lastFlight
        showNextFlight = true
      }, label: {
        LastViewedButton(name: lastFlight.flightName)
      }).buttonStyle(PlainButtonStyle())
    }
    Spacer()
  }
  .padding()
  // 4
  .frame(minWidth: 190, idealWidth: 190, maxWidth: 190,
         minHeight: 630, idealHeight: 630, maxHeight: .infinity)
  // 5
  .background(
    Image("welcome-background")
      .resizable()
      .aspectRatio(contentMode: .fill)
  )
}

OK, that’s a lot of code, but actually fewer lines than were there before. All the button views we had before are still there but wrapped differently.

  1. This view is going to be contained in the main NavigationView so does not need another one here. The ZStack, NavigationLinks, ScrollView and LazyVGrid have all been deleted.

  2. Instead of NavigationLinks, each of the different button views is wrapped in a Button that sets a displayState variable.

  3. The button style is set to PlainButtonStyle() to remove the standard macOS rounded rectangle button appearance and allow the view to set the size of the button.

  4. The VStack view has a frame modifier that sets the minimum, ideal and maximum width and height.

  5. A background modifier is used to apply the image as a background that will fill the view.

Sidebar properties

You will be seeing some errors now because body is accessing properties that do not exist, so scroll to the top of the WelcomeView struct and add this:

// 1
@SceneStorage("displayState") var displayState: DisplayState = .none
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?

// 2
var lastViewedFlight: FlightInformation? {
  if let id = lastViewedFlightID {
    return flightInfo.getFlightById(id)
  }
  return nil
}

And what’s happening here?

  1. In earlier chapters, you read about @AppStorage that provides a property wrapper for UserDefaults. @SceneStorage is similar to @AppStorage but stores settings for each window and not for the entire app. Since you might want to have multiple windows open showing different views, it makes sense to use @SceneStorage here. displayState keeps a record of what button you clicked, and that dictates what other view to display. lastViewedFlightID stores an optional Int with the ID of the flight that you looked at last.
  2. @SceneStorage and @AppStorage can only contain primitive types like String, Int, Double, Bool or enums that conform to these types. So you are storing the ID of the last viewed flight and using this computed property to get an optional FlightInformation object from it.

To fix the remaining error, add this enum to the end of MountainAirportMacApp.swift outside the struct:

enum DisplayState: Int {
  case none
  case flightBoard
  case searchFlights
  case awards
  case lastFlight
}

Now build and run the app to see your completed Mac sidebar.

Sidebar
Sidebar

NavigationViews in macOS

In an iPhone app, a NavigationLink inside a NavigationView slides the current view out and a new one in, while providing a way to go back. With a macOS app, this works differently. Because the views appear side-by-side, the NavigationView has to specify all of its views at the start. These views can change as the model data changes, but there must be a view in place when the NavigationView first appears, for each pane you want to display.

First, go to ContentView.swift and replace the body contents with this:

// 1
NavigationView {
  // 2
  WelcomeView()
  Text("Flight info goes here")
}
// 3
.navigationTitle("Mountain Airport")

Going through this code:

  1. The outermost view is now a NavigationView.
  2. Inside the NavigationView are two views that will appear side-by-side with one of them being a placeholder for now.
  3. The NavigationView is given a title which will appear as the window title.

Build and run and you can see how the window is starting to come together. You will need to make the window wider to see the second view.

Navigation View
Navigation View

You can resize the sidebar by dragging on the divider, but if you collapse it completely, you will not be able to get it back. To get around this bug, you can add a pre-configured menu item to your app.

Go to MountainAirportMacApp.swift and add this modifier to the WindowGroup:

// 1
.commands {
  // 2
  SidebarCommands()
}

And what do these few lines do?

  1. A commands modifier is how you add menus to your app as you saw in the previous chapter.

  2. SidebarCommands() is a pre-defined CommandGroup that adds a menu item and keyboard shortcut to the View menu, for toggling the sidebar.

Displaying the data views

Right now, the second pane of the NavigationView is displaying a placeholder Text view, but in this app, it will have to choose what to display based on the setting of displayState:

  • none: EmptyView
  • flightBoard: FlightStatusBoard + FlightDetails
  • searchFlights: SearchFlights
  • awards: AwardsView
  • lastFlight: FlightDetails (for last viewed flight)

Setting up properties

Before you can set this up, ContentView is going to need the data to pass to these other views, so add these properties to the top of the ContentView struct:

// 1
@StateObject var flightInfo = FlightData()

// 2
@SceneStorage("displayState") var displayState: DisplayState = .none
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?
@SceneStorage("selectedFlightID") var selectedFlightID: Int?

// 3
var selectedFlight: FlightInformation? {
  if let id = selectedFlightID {
    return flightInfo.getFlightById(id)
  }
  return nil
}

var lastViewedFlight: FlightInformation? {
  if let id = lastViewedFlightID {
    return flightInfo.getFlightById(id)
  }
  return nil
}

And what are all these?

  1. The main data model for the list of flights at the airport is stored in flightInfo. This is initialized here as an @StateObject. ContentView owns this data object and can pass it to other views.
  2. As in WelcomeView.swift, @SceneStorage holds the window specific settings. selectedFlightID is the only new one here.
  3. These two computed properties use the @SceneStorage properties to get flight information from the main model.

Remove the @StateObject var flightInfo property from WelcomeView.swift and replace it with this:

var flightInfo: FlightData

You also need to edit the preview to this:

WelcomeView(flightInfo: FlightData())
  .previewLayout(.fixed(width: 190, height: 630))

This gives the preview some data and sets its width and height to a column layout that will be more like how it appears in the app itself.

flightInfo will now be supplied to WindowView by ContentView, so jump over to ContentView.swift and change WelcomeView() to:

WelcomeView(flightInfo: flightInfo)

Choosing the view

Now that the data is ready for use, replace the Text placeholder view in ContentView.swift with this:

// 1
switch displayState {
  case .none:
    // 2
    EmptyView()
  case .flightBoard:
    // 3
    HStack {
      FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
      FlightDetails(flight: selectedFlight)
    }
  // 4
  case .searchFlights:
    SearchFlights(flightData: flightInfo.flights)
  case .awards:
    AwardsView()
  case .lastFlight:
    FlightDetails(flight: lastViewedFlight)
}

Here is what this code is doing:

  1. Which view to display in the main part of the window is decided by switching over the possible states for displayState.
  2. If no displayState has been set, as it will be when the app opens for the first time, an EmptyView is used so that the NavigationView still has the two views it needs to dictate its structure.
  3. The flight board will display an HStack with two internal views.
  4. The other options display the appropriate views as discussed earlier.

Now that you have added all that, Xcode is showing errors. This is because you are passing optional values to the FlightDetails view and it is expecting non-optionals. Expand the FlightDetails group, open up FlightDetails.swift and make these changes:

Replace the two properties at the top with this:

var flight: FlightInformation?
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?

This sets the flight to an optional and tells this view to use the @SceneStorage setting for the last viewed flight.

Command-click on the VStack and select Make Conditional. Type in let flight = flight in place of true.

Note: Sometimes you can Command-click on a view or open the Library and not see all the expected options. In this case, check that the canvas preview is open. It does not have to be active, but it has to be open to show all the options.

Move the onAppear modifier up to just under the line that sets the navigationTitle so that it is inside the if let and change its action to:

lastViewedFlightID = flight.id

which makes it set the @SceneStorage variable.

And finally, add these two frame modifiers to the ZStack:

.frame(minWidth: 350)
.frame(minHeight: 350)

They will ensure that this view never gets too small to display everything it needs to.

You may feel like there has been a lot of work to get this far, but there is a mass of code that you have not touched that is just working.

Flight Status

Build and run the app. Click on Flight Status and test out the tabs and the Hide Past toggle. Clicking on a flight shows a popover or maybe even two so that is something you are going to have to fix.

Flight Status
Flight Status

But before you start on that, open a new window in your app and click Flight Status there. You can select different flights in each window and you can have different settings for Hide Past but when you change the tabs in one window, that selection is applied to all open windows.

Expand the FlightStatusBoard group and open FlightStatusBoard.swift. At the top of the struct, you will see an @AppStorage property that stores the selected tab. Change @AppStorage to @SceneStorage to make selectedTab a window setting instead of an app setting. Build and run again and test out two different windows. Now you can select a different tab in each window.

Showing the selected flight

You already set up the FlightDetails view to show the flight that has been selected but to join this up to the list of flights, you need to change the list that displays all the flights so that it sets selectedFlightID when a flight is clicked.

Looking in FlightStatusBoard.swift, you can see that the body contains a TabView and each tab uses a FlightList view to display the relevant data. So that tells you that FlightList is the view you need to edit to change the list behavior.

Open FlightList.swift from the FlightStatusBoard group and add this to the top of the struct:

@SceneStorage("selectedFlightID") var selectedFlightID: Int?

This gives FlightList access to selectedFlightID so that the selection can be stored for this window whenever a flight is clicked.

Move down the file until you see the NavigationLink inside the List. Delete the NavigationLink and its contents and replace it with this:

// 1
Button(action: {
  selectedFlightID = flight.id
}, label: {
  // 2
  FlightRow(flight: flight)
})
// 3
.buttonStyle(PlainButtonStyle())

So what’s happening here?

  1. You have replaced a NavigationLink with a Button that sets the selectedFlightID.
  2. The contents of the Button is exactly the same as the contents of the NavigationLink.
  3. The button’s style is set to PlainButtonStyle() to remove the standard button appearance.

Attach a frame modifier to the ScrollViewReader to set a minimum width:

.frame(minWidth: 350)

You may have seen some weird scrolling as you changed tabs. The flight list scrolls to the next scheduled flight but sometimes this leaves blank spaces at the top of the list. This is because the scrollTo method sets the anchor point to .center and this doesn’t work so well in a Mac app. Change the scrollTo anchor to .top and your Mac will handle the scrolls much better.

Build and run the app again and test out the Flight Status. Click a flight to see its details.

Selected Flight
Selected Flight

The details appear, but what’s that white bar? Click it and a terminal map will animate in or out of view. The iOS version uses a custom transition to perform this animation and that is working perfectly, but the button is not styled to suit this display.

Open up FlightInfoPanel.swift and about half-way down the code, you will see a Button. Double-click on the opening bracket after the word Button and the entire button code will be selected, which tells you where the button ends. After that closing bracket, add this:

.buttonStyle(PlainButtonStyle())

Now try again and the buttons should look just right. You can now see the button animating as well as the terminal map. And you haven’t written a single line of animation code!

Great job! That was a big section, but now the app is really starting to come together.

Searching for flights

The first section of the app is now complete, so click the Search Flights button in the side bar to have a look at the next section.

Search
Search

The data is all there, the segmented picker at the top works and the search field works. But the display needs work and clicking on a flight crashes the app.

Fixing the display is going to be an easy one :]. Expand the SearchFlights group and open SearchResultRow.swift. This uses a Button to contain the data view and as you have done with all the Button views so far, you need to set the style of this button.

Underneath the Button and just before the .sheet line, add this modifier:

.buttonStyle(PlainButtonStyle())

Build and run the app again to see an immediate improvement.

However clicking on a flight still crashes the app and if you look at the crash report, the error is in FlightSearchDetails.swift where onAppear is setting lastFlightInfo.

If you scroll to the top of this struct, you will see it has an @EnvironmentObject property, but you are switching to use @SceneStorage to allow for multiple windows, so replace the @EnvironmentObject property with this:

@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?

And change the onAppear action to this:

lastViewedFlightID = flight.id

Build and run the app again, go to Search Flights and click on any flight.

Search 2
Search 2

A sheet pops up with the flight details which is great. Not so great is that the buttons on the sheets are using white text on a white background.

Click the one at the top-right of the sheet to dismiss it and go back to FlightSearchDetails.swift. Near the end of the struct, you will see a foregroundColor modifier that is setting the text color to white. The location of this modifier means that the setting is being applied to every subview in this view, including the buttons.

Don’t delete it completely as you still want the flight details text to be white. Cut the modifier out from where it is and paste it in immediately after two other views: FlightInfoPanel near the end of the struct and FlightDetailHeader near the top.

Build and run the app and test out the Search Flights.

Now, Select a flight and click any available buttons. On-Time History uses some custom drawing and animation for a great infographic display but it all just works, even though it was written for iOS!

If you can find a canceled departure, you can click Rebook Flight, which uses a standard system alert. The Check In for Flight button isn’t working yet because you commented out the actionSheet but apart form that, this view is now totally functional.

The styling of the controls at the top of the flights list could be improved and the sheet would be better with a set frame, but I will leave that as a challenge for you.

Last viewed flight

Before you jump into fixing the awards view, notice how the Last Viewed Flight button appears after you have selected a flight in either the Flight Status or Search Flights sections.

You are probably expecting a long list of changes needed to get this working but guess what? You’ve already made all the changes needed for this. Click on it and try it out.

So this is a nice short section and there is only one more to do. On to the awards…

Awards view

If you click Your Awards, the app will crash reporting that it cannot find the AppEnvironment ObservableObject.

In the Models group, take a look at AppEnvironment.swift and you will see that most of this class is setting up the awards data structure. The data is all there, but you need to pass it to the awards UI.

Open up AwardsView.swift from the AwardsView group and find the AwardsView struct. It is expecting to get an AppEnvironment object passed to it as an @EnvironmentObject. But now that you are using @SceneStorage for the other properties, this is the only view that needs to access AppEnvironment, so why not let it own that data itself?

Replace the @EnvironmentObject line with this:

@State var flightNavigation = AppEnvironment()

So now AwardsView has its own data model that it can display.

Build and run the app and click on Your Awards. No crashes anymore but the UI needs work.

Scroll to the top of AwardsView.swift to see the AwardGrid struct which lays out each section of the view. Each AwardCardView is contained within a NavigationLink, but you are going to get rid of this. Replace the contents of the ForEach with:

AwardCardView(award: award)
  .foregroundColor(.black)
  .aspectRatio(0.67, contentMode: .fit)

The ForEach now contains only theAwardCardView and its two modifiers. When you build and run the app, you can see all the awards in two grids, but they are not clickable.

Note: If you are not seeing all the images, make sure they have all been dragged from the 3x box to the 2x box in Assets.xcassets.

Awards
Awards

To make the awards clickable, open up AwardCardView.swift where you are going to add a sheet modifier to display the AwardDetails.

Add this property:

@State private var isPresented = false

isPresented will dictate whether the sheet is visible or not.

Command-click on the VStack and select Embed in HStack. You are not going to use an HStack, but this is an easy way to wrap a view making sure you get all the components and that the indentation is correct.

Now, replace the HStack line with this:

// 1
Button(action: {
  isPresented.toggle()
}, label: {

You will see an error at the end of the struct, but add this code just above the line with the error to make it go away:

// 2
)
// 3
.buttonStyle(PlainButtonStyle())
// 4
.sheet(
  isPresented: $isPresented,
  content: {
    AwardDetails(award: award)
  }
)

So these two chunks of code do these things:

  1. Create a button that toggles the isPresented variable to display the sheet.
  2. Close off the Button view, wrapping the VStack.
  3. Set the plain button style as usual.
  4. Use a sheet to display the AwardDetails for the selected award if isPresented is true.

Don’t run the app yet. There is one more important feature to add to this sheet — a way to dismiss it. On iOS, you can swipe a sheet down to get rid of it, but that doesn’t work on macOS, so every sheet must have a dismiss option.

Open AwardDetails.swift and add this property:

@Environment(\.presentationMode) var presentationMode

This gives the view access to an environment property that can be used to dismiss the sheet.

Next, add this inside the VStack before the first Image:

// 1
HStack {
  Spacer()
  Button(action: {
    // 2
    presentationMode.wrappedValue.dismiss()
  }, label: {
    // 3
    Image(systemName: "xmark.circle")
      .font(.largeTitle)
  })
  // 4
  .buttonStyle(PlainButtonStyle())
}

So what’s going on here?

  1. You are adding an HStack with a Spacer as the first view so that the Button will be pushed to the right.
  2. The button’s action uses presentationMode to dismiss the sheet.
  3. The UI of the button is an Image using a system icon from SF Symbols which is sized using the font modifier.
  4. And I bet you didn’t see this coming… the button style is set to PlainButtonStyle().

Build and run again and now you can view the awards, click on an award to display its details and click the X to close the sheet.

Awards 2
Awards 2

And that’s it! You’ve done it. The app now has all the features of its iOS counterpart.

Challenges

Challenge 1: Styling

What about adding some conditional styling to the sidebar buttons to show which of the main views is selected? And the Search Flights display and its popups could do with some modifications to the look and feel and to the sizes. Don’t forget to check how things look in both light and dark modes.

Challenge 2: Check-in alert

Remember how you commented out the actionSheet in FlightSearchDetails.swift? See if you can work out how to replace this with an alert.

Challenge 3: Converting other apps to Mac

Congratulations! You made it. You started with an iOS app and you re-used code and assets to make a Mac app. You have learned how to fix the bugs caused by importing iOS code and how to set up images to work on a Mac.

Select another interesting iOS project, maybe one of your own projects, one of the other raywenderlich.com apps or perhaps something open source, and see if you can convert it to a Mac app.

Key points

  • There is a lot of iOS code around and you can use a great deal of it in your macOS apps with little or no changes.
  • macOS apps can have multiple windows open at once, so you need to make sure that your settings apply correctly. Do they need to be app-wide or per window?
  • iOS apps have fixed-sized views, but on the Mac, you must be aware of different possible window sizes.
  • When faced with a conversion task, take it bit by bit. Get the app-building without error first, even if this means commenting out some functionality. Then go through the interface one section at a time, checking to see what works and what has to be changed.
  • You imported 34 Swift files into your app. Twenty-one of them required no editing and only four of the 13 changed files had significant numbers of changes! That has saved an enormous amount of time and effort.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.