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.
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:
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.
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.
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.
- 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())
}
- 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!
- StackNavigationViewStyle is unavailable in macOS - WelcomeView.swift:
For this app the default style will be fine, so you can delete the navigationViewStyle modifier.
- 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.
- 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.
- 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.
- 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.
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.
-
This view is going to be contained in the main
NavigationViewso does not need another one here. TheZStack,NavigationLinks,ScrollViewandLazyVGridhave all been deleted. -
Instead of
NavigationLinks, each of the different button views is wrapped in aButtonthat sets adisplayStatevariable. -
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. -
The
VStackview has aframemodifier that sets the minimum, ideal and maximum width and height. -
A
backgroundmodifier 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?
- In earlier chapters, you read about
@AppStoragethat provides a property wrapper forUserDefaults.@SceneStorageis similar to@AppStoragebut 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@SceneStoragehere.displayStatekeeps a record of what button you clicked, and that dictates what other view to display.lastViewedFlightIDstores an optionalIntwith the ID of the flight that you looked at last. -
@SceneStorageand@AppStoragecan only contain primitive types likeString,Int,Double,Boolor 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 optionalFlightInformationobject 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.
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:
- The outermost view is now a
NavigationView. - Inside the
NavigationVieware two views that will appear side-by-side with one of them being a placeholder for now. - The
NavigationViewis 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.
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?
-
A
commandsmodifier is how you add menus to your app as you saw in the previous chapter. -
SidebarCommands()is a pre-definedCommandGroupthat 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?
- The main data model for the list of flights at the airport is stored in
flightInfo. This is initialized here as an@StateObject.ContentViewowns this data object and can pass it to other views. - As in WelcomeView.swift,
@SceneStorageholds the window specific settings.selectedFlightIDis the only new one here. - These two computed properties use the
@SceneStorageproperties 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:
- Which view to display in the main part of the window is decided by switching over the possible states for
displayState. - If no
displayStatehas been set, as it will be when the app opens for the first time, anEmptyViewis used so that theNavigationViewstill has the two views it needs to dictate its structure. - The flight board will display an
HStackwith two internal views. - 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.
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?
- You have replaced a
NavigationLinkwith aButtonthat sets theselectedFlightID. - The contents of the
Buttonis exactly the same as the contents of theNavigationLink. - 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.
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.
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.
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.
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:
- Create a button that toggles the
isPresentedvariable to display the sheet. - Close off the
Buttonview, wrapping theVStack. - Set the plain button style as usual.
- Use a sheet to display the
AwardDetailsfor the selected award ifisPresentedis 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?
- You are adding an
HStackwith aSpaceras the first view so that theButtonwill be pushed to the right. - The button’s action uses
presentationModeto dismiss the sheet. - The UI of the button is an
Imageusing a system icon from SF Symbols which is sized using thefontmodifier. - 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.
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.