Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

If you’ve worked through the early chapters of this book, you’ve built several iOS apps. In the previous chapter, you’ve also made a document-based Mac app. But in this chapter, you’ll make a macOS app from an iOS app. You’ll use the code, views and assets from an iOS project to make it.

Most Swift and SwiftUI tutorials, and examples on the internet, are for iOS, primarily specifically for iPhones. So knowing how to re-use the code in an iOS project to create a real Mac app is a valuable skill.

Getting Started

Open the starter project from the downloaded materials for this chapter, which is the iOS app you’ll convert. You may have already built this app in earlier chapters, but please use this starter project even if you have.

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

iOS app screens
iOS app screens

The iOS version uses a common navigational pattern where the initial screen offers a selection of choices. A NavigationSplitView shows the options, displaying other views. These secondary views sometimes have even more options, including full views, sheets or dialogs.

For the Mac version, where you can assume much wider screens, you’ll have the 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’ll be a lot of editing, which can be difficult to explain and follow, but if you get lost, open 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 by selecting SwiftUI for the Interface and Swift for the Language. Call the app MountainAirportMac and save it.

Importing Code Files

To start, switch to Finder and open the MountainAirport folder inside the starter project folder.

Project navigator after imports
Kxoxetr vikapamih oyhuj usyuntz

Importing Assets

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

Imported assets
Islirdas efmowc

Image assets
Ejake iwqubl

Fixing the Build Errors

You’ve 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.

.listStyle(.inset(alternatesRowBackgrounds: true))
.toolbar {
  Toggle("Hide Past", isOn: $hidePast)
}
First run
Nusch kug

Diagnosing the Crash

Don’t worry — this sort of thing happens, and learning how to track down crashes is a useful skill, especially as Xcode isn’t always very helpful. In this case, the crash report points to @main in MountainAirportMacApp.swift, which tells you nothing useful. Time for some detective work.

Using Text placeholders
Icaqd Xevj kyoqopozlihh

Without the NavigationStack.
Lippiuf vqe QiqehereonFgegy.

Navigation in macOS

A NavigationLink inside a NavigationStack slides the current view out and a new one in, while providing a way to go back. This works great in an iPhone app, but this isn’t always the best way in a macOS app with more available space.

// 1
@SceneStorage("selectedFlightID") var selectedFlightID: Int?
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?

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

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

Modifying the Flight List

Next, open FlightList.swift and replace body with:

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

var body: some View {
  ScrollViewReader { scrollProxy in
    // 2
    List(flights, selection: $selectedFlightID) { flight in
      FlightRow(flight: flight)
        // 3
        .tag(flight.id)
    }
    .onAppear {
      // 4
      scrollProxy.scrollTo(nextFlightId, anchor: .top)
    }
  }
}

Showing the Selected Flight

Open FlightDetails ▸ FlightDetails.swift. This is the view that displays the details of a flight, either directly from the flight list or as the last viewed flight. Right now, it always expects a valid FlightInformation object, but this may be nil in the new arrangement.

var flight: FlightInformation?
Make Conditional
Ditu Vajfitounim

Text("Select a flight…")
  .foregroundColor(.white)
if flight?.terminal == "A" {
@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?
.onChange(of: flight) { _ in
  lastViewedFlightID = flight?.id
}
case .showFlightStatus:
  // 1
  HSplitView {
    // 2
    FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
    // 3
    FlightDetails(flight: selectedFlight)
  }
Selected flight
Fupoxkux wdalnj

Styling the Flight Details

You’ll first notice the Show Terminal Map button. It shows the map and all the animations work perfectly, but the button looks wrong. A macOS button is styled differently from an iOS button by default, but you can fix this.

.buttonStyle(.plain)

Setting Frames

With iOS apps, the available space is pre-defined. An iPhone app fills the entire screen, and most iPad apps do the same. With Mac apps, a screen can be huge, so as an app developer, it’s your responsibility to set sizes for components in your app.

Terminal details
Zijsimag cuyeonh

.frame(height: 300)
.frame(minWidth: 400, minHeight: 400)
.frame(minWidth: 300, minHeight: 400)
.frame(minWidth: 250, minHeight: 400)
Narrow window
Hizrod gedlav

.frame(minWidth: 700)

Searching for Flights

The app’s first section is complete, so click the Search Flights button in the sidebar to look at the next section.

Search
Fieqwn

.buttonStyle(.plain)
Spacer()
.contentShape(Rectangle())
Search result
Fousyv tirayw

.frame(width: 400, height: 600)
Search details
Seuftz lofuotq

Showing the Flight History

But now that FlightTimeHistory shows the on-time history, you’ve got a problem. How do you get rid of it? On iOS, you’d swipe down, but that doesn’t work on macOS, so you need to find another solution. For starters, return to Xcode and click the Stop button to quit the app.

Button("On-Time History") {
  showFlightHistory.toggle()
}
.popover(isPresented: $showFlightHistory) {
  FlightTimeHistory(flight: flight)
}
On-time popover
Ov-qisa nabodup

Display Dialog Boxes

View a canceled flight and click Rebook Flight. This shows an alert with two entry fields — one of them a secure field for passwords. There are two problems here. Try typing something in the fields. They use white text, which is almost invisible. This is because the parent view has set the .foregroundColor to white.

.foregroundColor(.primary)
Password entry
Hegzcezq itsnn

Awards View

When you click Your Awards, you see a mess:

Awards in a mess
Ojopbh id a poky

.buttonStyle(.plain)
Awards
Eriqqk

ScrollView(showsIndicators: false) {
Awards
Orikqf

The Last Viewed Flight

The Last Viewed Flight button appears in the sidebar after you select a flight in the Search Flights section. Clicking it shows the list of flights but no details about the flight.

@SceneStorage("lastViewedFlightID") var lastViewedFlightID: Int?
lastViewedFlightID = flight.id
// 1
if let flight = lastViewedFlight {
  // 2
  buttons.append(
    ViewButton(
      id: .showLastFlight,
      title: "\(flight.flightName)",
      subtitle: "The Last Flight You Viewed",
      imageName: "suit.heart.fill"
    )
  )
}
case .showLastFlight:
  // 1
  if let lastFlight = lastViewedFlight {
    // 2
    HSplitView {
      // 3
      FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
      // 4
      FlightDetails(flight: lastFlight)
    }
  }
Last viewed flight
Cuvg bouyap jsiwtg

Challenge

In SearchFlights ▸ FlightSearchDetails.swift, swap the alert that displays the password for a sheet that hides it correctly.

Key Points

  • There’s 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 to see what works and what you have to change.
  • You imported 37 Swift files into your app. 23 of them required no editing, and only 3 of the 14 changed files had significant numbers of changes! That has saved an enormous amount of time and effort, but you still ended up with a native Mac app.

Where to Go From Here?

Congratulations! You made it. You started with an iOS app, and you re-used code and assets to make a Mac app. You’ve learned how to fix the bugs caused by importing iOS code and how to adjust the user interface to suit a Mac.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now