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

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.

Then select the following folders and files, 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.

  1. All the .swift files in the MountainAirport folder.
  2. AwardsView folder.
  3. FlightDetails folder.
  4. FlightStatusBoard folder.
  5. Misc folder.
  6. Models folder.
  7. SearchFlights folder.

After you move the files, delete MountainAirport.swift from your project. This is an iOS-specific file that you don’t need for your new Mac app.

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

Project navigator after imports
Project navigator after imports

You’ve now got a lot of code from the iOS app in your macOS app. You can assume that the model classes and structures mostly work as expected and don’t need you to change anything. Your main task is to change the SwiftUI code to make the user interface work on a Mac. But you’ve already saved yourself a lot 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 the iOS app uses, primarily the app icon and any images used in the app’s UI.

First, go to Assets.xcassets in your Xcode Project navigator and then open the Assets.xcassets folder in the iOS project’s Finder window. If you’re not showing file extensions, these may appear as Assets without the .xcassets extension. Now, drag every folder inside this folder into your list of assets.

Imported assets
Imported assets

This adds all the assets, but an iOS project configures its assets differently from a macOS project, so now you’ve got some housekeeping to do, starting with the app icon.

In the assets list, there’s AppIcon, part of the app template, and AppIcon 1, which you imported. Unfortunately, iOS and macOS have 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 correct image sizes, but for now, you’ll need 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.

Note: For an iOS app, you supply square icons, and iOS rounds the corners for you. Mac app icons have rounded corners with transparent padding, which you have to apply. If you’re releasing a Mac version of an iOS app, you’d need to re-design the icon, but the square icon will do for now.

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

In the Devices section, Universal is checked, meaning this image works on any Apple device. But the image is in the 3x box for the high-resolution iPhones and iPads, and 3x images don’t 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 3x images, not forgetting the ones in the award-images folder.

Image assets
Image assets

Now it’s time to build!

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.

Press Command-B to build the app, but don’t panic when it doesn’t work. You have to expect this when you import code that was written for a different platform.

Open the Issue navigator to see the first set of problems. There’s a report of an invalid redeclaration of ContentView_Previews. The iOS app uses WelcomeView as the initial view, but the preview provider in WelcomeView.swift is still called ContentView_Previews.

To set the Mac app to open with WelcomeView, open MountainAirportMacApp.swift and replace ContentView() with WelcomeView(). Now you can delete ContentView.swift and try again. Press Shift-Command-K to clean the build folder and then Command-B to build. Doing a clean build after fixing errors is always a good idea, especially if you deleted any files.

The next error is “Cannot find type UIColor in scope” which is in FlightInformation.swift. Click the error in the Issue navigator to jump to the file and the location of the error.

UIColor is a UIKit color object. The equivalent in AppKit is NSColor, but the Mac version doesn’t use this property, so delete the timelineColor computed property to eliminate the error.

Subsequent builds give these errors in either order:

  • InsetGroupedListStyle 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’ve looked at the options, replace the error line with:

.listStyle(.inset(alternatesRowBackgrounds: true))

This styles the list and makes the rows easy to distinguish.

  • navigationBarItems(trailing:) is unavailable in macOS — FlightStatusBoard.swift:

Instead of a navigation bar item for this Toggle, you’ll use the Mac toolbar. Replace the navigationBarItems modifier and its contents with this:

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

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

Press Command-B again, and the app builds with no errors this time.

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!

Now, press Command-R to run the Mac app.

Expand the window and the sidebar to see the three navigation buttons and the animated plane. Click Your Awards to see the awards display. It isn’t pretty, but it works! Search Flights is similar — it works, but the display is messy. In the next sections, you’ll make it look much better.

First run
First run

Now click Flight Status. Not so good. This is a crasher. It needs fixing before anything else.

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.

Open WelcomeView.swift and check out what happens when you click Flight Status. The ViewButton has an id of FlightViewId.showFlightStatus, and clicking the button sets selectedView to this. The NavigationSplitView then shows FlightStatusBoard in its detail.

Following the clues, open FlightStatusBoard ▸ FlightStatusBoard.swift. The main view here is a TabView with three tabs. Each tab shows a FlightList with a different set of flights. A good way to track down display bugs is to replace suspicious views with placeholder Text views. If the Text view works, you’ve found the culprit.

In FlightStatusBoard.swift, you’ll replace each use of FlightList with Text and see what happens. Double-click the opening parenthesis after the first FlightList to select the view and its parameters. Press Command-/ to comment it out, and on the line above, insert Text("Arrivals List"). Place the cursor before .tabItem on the last commented line and press Return to re-enable that modifier.

Repeat this with the other two FlightLists, inserting Text("Complete List") and Text("Departures List") and re-enabling the tabItem for each.

Build and run the app again. This time clicking Flight Status works, and the three tabs show their Text views:

Using Text placeholders
Using Text placeholders

Now you know the problem is in FlightList. Good detecting, Sherlock!

Note: You may see more errors appearing in the Issues navigator now, but you can ignore them. They don’t stop the app from running, and they’ll disappear as you work through the rest of this chapter.

Open FlightStatusBoard ▸ FlightList.swift and look at body. It uses a NavigationStack to display a ScrollViewReader and then a List — maybe this is a problem on macOS.

To test this out, comment out the NavigationStack line. Then comment out its closing brace on the line above onAppear.

Back in FlightStatusBoard.swift, press Command-Z repeatedly until you’ve re-enabled the three FlightList views.

Build and run to see what happens:

Without the NavigationStack.
Without the NavigationStack.

The list appears, the tabs show the correct data and the Hide Past toggle does what it should. Great work! You’ve found the exact cause of the crash, and now you can fix it.

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.

Here, it would be better to divide the display, showing the list of flights on the left and the selected flight on the right. The crash is due to a bug in SwiftUI for Mac, but avoiding it gives a better result for a Mac app.

To make this happen, open WelcomeView.swift. This uses FlightStatusBoard in two places, once for the flight status and again for the last viewed flight, if there is one.

But before you change this, you need to add properties to store the selected flight and the last viewed flight.

Inside WelcomeView, before body, insert this:

// 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
}

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 users may want multiple windows open showing different flights, it makes sense to use @SceneStorage here. selectedFlightID stores the id of the flight selected in FlightStatusBoard. lastViewedFlightID stores the id of the flight that you looked at last. Both are optional Ints.
  2. @SceneStorage and @AppStorage can only contain primitive types like String, Int, Double, Bool, or enums that conform to these types. So you’re using these computed properties to get optional FlightInformation objects from the ids.

When FlightList used a NavigationStack, it was able to show the selected flight using a NavigationLink. Without that, you need to record the selected flight and display it manually if it exists.

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)
    }
  }
}

This adds some code and removes some:

  1. Give FlightList access to the @SceneStorage property that holds the id of the selected flight.
  2. Bind this as the selection parameter for the list.
  3. Assign a tag to each line on the list. When the user clicks a list item, you use this to set selectedFlightID.
  4. This scrollTo was there before, but a top anchor works better on macOS.

So now, FlightList can set selectedFlightID and WelcomeView can detect this. The next step is to display the selected flight.

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.

Start at the top of FlightDetails and add a question mark to make flight an optional:

var flight: FlightInformation?

This causes errors down below. Command-click on VStack and select Make Conditional from the popup menu:

Make Conditional
Make Conditional

Replace the true placeholder with let flight.

In the else block, replace EmptyView() with:

Text("Select a flight…")
  .foregroundColor(.white)

To solve the next error, replace the line with:

if flight?.terminal == "A" {

This uses optional chaining to check the value of terminal only when there’s a valid flight.

The final error requires two steps. First, add this property declaration to FlightDetails:

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

This gives it access to the @SceneStorage property that records the last viewed flight in this window.

Then replace the onAppear block with:

.onChange(of: flight) { _ in
  lastViewedFlightID = flight?.id
}

Whenever you view a new flight in FlightDetails, this detects it and sets lastViewedFlightID to match.

This has been a lot of work, but you’re nearly there. The data is in place, and FlightList and FlightDetails are ready. Now back to WelcomeView.swift to use them.

Replace the showFlightStatus case in the NavigationSplitViews detail with:

case .showFlightStatus:
  // 1
  HSplitView {
    // 2
    FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
    // 3
    FlightDetails(flight: selectedFlight)
  }

This is what’s changed.

  1. The displayed view is now an HSplitView, which divides the view into two parts arranged side-by-side.
  2. The first part is the same as before.
  3. The second half shows FlightDetails, connected to the selectedFlight computed property. When the user clicks a flight in FlightList, the list selection changes selectedFlightID. This updates selectedFlight and SwiftUI displays the new data inside FlightDetails.

Build and run the app now. Click Flight Status in the sidebar and then select different flights:

Selected flight
Selected flight

Hooray! No crash and you can see flight details. But there are some things to fix in the details display.

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.

Open FlightDetails ▸ FlightInfoPanel.swift and look for Button. After the label, on the line before if showTerminal {, add this line:

.buttonStyle(.plain)

That makes the button look better.

But there’s another problem.

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.

Run the app again, select a flight and click anywhere in the flight details display outside the button:

Terminal details
Terminal details

A sheet with information about the terminal pops up, but it’s much too tall. Click anywhere in the sheet to dismiss it. These sheets are in FlightStatusBoard ▸ TerminalAView.swift and FlightStatusBoard ▸ TerminalBView.swift. The height is set by the background image, which is sized to fill a tall iPhone screen.

The Image in each of these files has a frame modifier. Replace this in both files with the following:

.frame(height: 300)

This fixes the height to a setting tall enough to show all the information without wasting space.

You need to set limits for the main window as well. Try moving the various parts around. Shrink and expand the window, resize the sidebar and adjust the slider in the middle of the HSplitView. Each of those parts needs a frame modifier.

Start in FlightDetails ▸ FlightDetails.swift. Find the .contentShape(Rectangle()) line and insert this before it:

.frame(minWidth: 400, minHeight: 400)

That makes sure the details can never get too small in either dimension.

Next, open FlightStatusBoard ▸ FlightStatusBoard.swift. Double-click the curly brace on the TimelineView line to select the entire block and locate the closing curly brace. After the closing brace, add this:

.frame(minWidth: 300, minHeight: 400)

Finally, the sidebar. Open WelcomeView.swift and insert this line after .listStyle(.plain):

.frame(minWidth: 250, minHeight: 400)

Run the app again, select a flight and re-arrange the components. Then try shrinking the entire window. Despite setting minimum limits for each component, the overall width can still need to be bigger. The height is good, but not the width:

Narrow window
Narrow window

The final step is to open MountainAirportMacApp.swift and add this modifier to WelcomeView():

.frame(minWidth: 700)

This is small enough to let the sidebar collapse but keeps the HSplitView intact.

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
Search

The data is all there, the segmented picker at the top works, and the search field in the toolbar allows you to select from a list of cities. But the display needs work, and clicking on a flight shows another tall sheet.

Fixing the display is the first step. Expand the SearchFlights group, and open SearchResultRow.swift. This uses a Button to contain the data view, and as before, you need to set the style of this button.

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

.buttonStyle(.plain)

Build and run the app again to see an immediate improvement. However, you have to click some text or the icon to show the flight information. It would be good to be able to click anywhere in the row.

Open SearchFlights ▸ FlightSearchSummary.swift. Find the closing brace at the end of the HStack block. On the line before this, insert:

Spacer()

And on the line after the brace, add:

.contentShape(Rectangle())

The Spacer makes the HStack fill the row, and setting the contentShape makes the entire rectangle clickable.

Build and run the app again, search the flights and click any row:

Search result
Search result

Like before, the background image makes the sheet too long.

Open SearchFlights ▸ FlightSearchDetails.swift and scroll down to near the end. Before .interactiveDismissDisabled(), add this line:

.frame(width: 400, height: 600)

This locks the size of this popup sheet, which is often a good thing to do in a Mac app. It stops people from resizing an element that doesn’t need resizing.

Build and run the app and click Search Flights. Select a flight and click On-Time History:

Search details
Search details

This view uses some custom drawing code to make a pie chart, and it all just works, even though that’s iOS code!

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.

In FlightSearchDetails.swift, find the On-Time History button. This button toggles a Boolean called showFlightHistory and that property controls the display of FlightTimeHistory in a sheet.

Change this Button to show a popover like this:

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

Now try again, and you’ll be able to click anywhere outside the view to dismiss it:

On-time popover
On-time popover

But the display isn’t right. You can’t see the beginning of each line of text. Again, the culprit is the background image.

Open SearchFlights ▸ FlightTimeHistory.swift and find the Image containing background-view. This has an aspectRatio modifier that expands the image past the popover’s edges. This means that the start of the text is outside the view, where you can’t see it.

Delete the .aspectRatio(contentMode: .fill) line to solve this one.

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.

In SearchFlights ▸ FlightSearchDetails.swift, look for the Rebook Flight button. It has its contents and its message. After message, insert this line:

.foregroundColor(.primary)

This sets the foregroundColor for the alert to the system’s primary color: black for light mode and white for dark mode. The rest of the view still has the required white foreground color.

Run the app again, search for a canceled flight and try entering some information. And now you can see the second problem:

Password entry
Password entry

My super secret password isn’t as secret as I’d hoped!

This is another macOS bug. The workaround is to use a sheet here instead of an alert, but I’ll leave that as a challenge for you.

Search for a departing flight that hasn’t left yet. This shows a Check In for Flight button that uses a confirmationDialog. This works without any modifications, and now this view is totally functional.

On to the next…

Awards View

When you click Your Awards, you see a mess:

Awards in a mess
Awards in a mess

Open AwardsView ▸ AwardsView.swift to see the AwardGrid struct, which lays out each section of the view. Each AwardCardView is inside a NavigationLink. The thing is that a NavigationLink is basically a button, and these links use the standard macOS button appearance.

Find the closing brace at the end of the NavigationLink and after that, insert:

.buttonStyle(.plain)

Build and run now, and it looks a lot better:

Awards
Awards

Click any award card to show the award in a new view with a back button in the toolbar.

Note: If you don’t see all the images, drag them all from the 3x box to the 2x box in Assets ▸ award-images.

Another problem is that the section headers use white text on a grey background. Delete the .background modifier from the Section header. The white text shows up nicely on the background image.

The scroll bar is rather ugly and set in from the edge of the window, which looks strange. One solution is to hide it.

Find ScrollView in AwardsView ▸ body and replace that line with:

ScrollView(showsIndicators: false) {

Run the app again to check out the improved look:

Awards
Awards

You’ve completed another part of the app!

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.

Open SearchFlights ▸ FlightSearchDetails.swift and scroll down to the onAppear modifier. This sets a published property in an @EnvironmentObject. For the Mac app, where the user can have multiple windows open, you want to use the @SceneStorage property you set up earlier.

Back up at the top of FlightSearchDetails, add the property:

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

Scroll down to onAppear again, and replace the contents with:

lastViewedFlightID = flight.id

So now, whenever the user shows the details for a searched flight, this property is set to the id of that flight.

Next, open WelcomeView.swift. The first step is to display the button if there’s a last-viewed flight. At the top of WelcomeView, there’s a computed property called sidebarButtons that supplies an array of buttons to display. The last entry checks for a last-viewed flight.

Replace the last button entry with:

// 1
if let flight = lastViewedFlight {
  // 2
  buttons.append(
    ViewButton(
      id: .showLastFlight,
      title: "\(flight.flightName)",
      subtitle: "The Last Flight You Viewed",
      imageName: "suit.heart.fill"
    )
  )
}

In this code, you:

  1. Use the lastViewedFlight computed property to check if there’s a value in lastViewedFlightID that points to a flight.
  2. If there is, construct the ViewButton as before.

To handle what happens when you click this button, scroll down to the switch that controls the detail part of the NavigationSplitView.

Replace the showLastFlight case with:

case .showLastFlight:
  // 1
  if let lastFlight = lastViewedFlight {
    // 2
    HSplitView {
      // 3
      FlightStatusBoard(flights: flightInfo.getDaysFlights(Date()))
      // 4
      FlightDetails(flight: lastFlight)
    }
  }

How does this show the flight?

  1. As with the button, check that there’s a flight to show.
  2. Display an HSplitView, like for the flight status display.
  3. Show the FlightStatusBoard listing all of today’s flights.
  4. Add a FlightDetails view, passing in the last viewed flight.

Build and run the app. Search for a flight and view its details to see it appear in the sidebar. Then close the details display, and click the last button in the sidebar to jump to the flight list and details:

Last viewed flight
Last viewed flight

Open a second window from the File menu. It lets you select different flights in the flight status board and have a different last viewed flight. Because you used @SceneStorage, the app remembers your last viewed flight when you quit and restart.

There’s one oddity remaining. With two windows open, display Flight Status in both. Now change the tab, switching between Arrivals, All and Departures. Both windows change to show the same tab.

Open FlightStatusBoard ▸ FlightStatusBoard.swift and check the properties at the top. The selectedTab property is marked with the @AppStorage wrapper. Change this to @SceneStorage to make the windows independent.

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

Challenge

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

Hint: Create a new view for the sheet’s contents and display it instead of the alert when rebookAlert is true.

Try this yourself, but check out the project in the challenge folder if you get stuck. All the changes are in FlightSearchDetails.swift.

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.

There are lots of ways you can make the app more Mac-like:

  • Add menus and menu items to perform some of the commands.
  • Style elements to suit the Mac better — don’t forget to test in dark and light modes.
  • Apply more frames to make the windows behave more consistently.

Then, select another interesting iOS project, maybe one of your own, one of the other Kodeco apps or perhaps something open-source. See if you can use these techniques to convert it to a Mac app.

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.