Porting Your iOS App to macOS
Learn how to port iOS apps to macOS. If you’re developing apps for iOS, you already have skills that you can use to write apps for macOS! By Andy Pereira.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Porting Your iOS App to macOS
30 mins
- Getting Started
- Adding the Assets
- Separation of Powers
- Models
- Creating the User Interface
- Configuring the Table View
- Adding a Delegate and Data Source
- Images and Text
- Rating a Beer
- Adding and Removing Beers
- Finishing The UI
- Adding the Code
- Adding Data to the Table View
- Removing Entries
- Handling Images
- Final Touches
- Where to Go From Here?
Adding the Code
Whew! Now you’re ready to code. Open ViewController.swift and delete the property named representedObject
. Add the following methods below viewDidLoad()
:
private func setFieldsEnabled(enabled: Bool) {
imageView.isEditable = enabled
nameField.isEnabled = enabled
ratingIndicator.isEnabled = enabled
noteView.isEditable = enabled
}
private func updateBeerCountLabel() {
beerCountField.stringValue = "\(BeerManager.sharedInstance.beers.count)"
}
There are two methods that will help you control your UI:
-
setFieldsEnabled(_:)
will allow you to easily turn off and on the ability to use the form controls. -
updateBeerCountLabel()
simply sets the count of beers in thebeerCountField
.
Beneath all of your outlets, add the following property:
var selectedBeer: Beer? {
didSet {
guard let selectedBeer = selectedBeer else {
setFieldsEnabled(enabled: false)
imageView.image = nil
nameField.stringValue = ""
ratingIndicator.integerValue = 0
noteView.string = ""
return
}
setFieldsEnabled(enabled: true)
imageView.image = selectedBeer.beerImage()
nameField.stringValue = selectedBeer.name
ratingIndicator.integerValue = selectedBeer.rating
noteView.string = selectedBeer.note!
}
}
This property will keep track of the beer selected from the table view. If no beer is currently selected, the setter takes care of clearing the values from all the fields, and disabling the UI components that shouldn’t be used.
Replace viewDidLoad()
with the following code:
override func viewDidLoad() {
super.viewDidLoad()
if BeerManager.sharedInstance.beers.count == 0 {
setFieldsEnabled(enabled: false)
} else {
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
}
updateBeerCountLabel()
}
Just like in iOS, you want our app to do something the moment it starts up. In the macOS version, however, you’ll need to immediately fill out the form for the user to see their data.
Adding Data to the Table View
Right now, the table view isn’t actually able to display any data, but selectRowIndexes(_:byExtendingSelection:)
will select the first beer in the list. The delegate code will handle the rest for you.
In order to get the table view showing you your list of beers, add the following code to the end of ViewController.swift, outside of the ViewController
class:
extension ViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return BeerManager.sharedInstance.beers.count
}
}
extension ViewController: NSTableViewDelegate {
// MARK: - CellIdentifiers
fileprivate enum CellIdentifier {
static let NameCell = "NameCell"
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let beer = BeerManager.sharedInstance.beers[row]
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: CellIdentifier.NameCell), owner: nil) as? NSTableCellView {
cell.textField?.stringValue = beer.name
if beer.name.characters.count == 0 {
cell.textField?.stringValue = "New Beer"
}
return cell
}
return nil
}
func tableViewSelectionDidChange(_ notification: Notification) {
if tableView.selectedRow >= 0 {
selectedBeer = BeerManager.sharedInstance.beers[tableView.selectedRow]
}
}
}
This code takes care of populating the table view’s rows from the data source.
Look at it closely, and you’ll see it’s not too different from the iOS counterpart found in BeersTableViewController.swift. One notable difference is that when the table view selection changes, it sends a Notification to the NSTableViewDelegate.
Remember that your new macOS app has multiple input sources — not just a finger. Using a mouse or keyboard can change the selection of the table view, and that makes handling the change just a little different to iOS.
Now to add a beer. Change addBeer()
to:
@IBAction func addBeer(_ sender: Any) {
// 1.
let beer = Beer()
beer.name = ""
beer.rating = 1
beer.note = ""
selectedBeer = beer
// 2.
BeerManager.sharedInstance.beers.insert(beer, at: 0)
BeerManager.sharedInstance.saveBeers()
// 3.
let indexSet = IndexSet(integer: 0)
tableView.beginUpdates()
tableView.insertRows(at: indexSet, withAnimation: .slideDown)
tableView.endUpdates()
updateBeerCountLabel()
// 4.
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
}
Nothing too crazy here. You’re simply doing the following:
- Creating a new beer.
- Inserting the beer into the model.
- Inserting a new row into the table.
- Selecting the row of the new beer.
You might have even noticed that, like in iOS, you need to call beginUpdates()
and endUpdates()
before inserting the new row. See, you really do know a lot about macOS already!
Removing Entries
To remove a beer, add the following code for removeBeer(_:)
:
@IBAction func removeBeer(_ sender: Any) {
guard let beer = selectedBeer,
let index = BeerManager.sharedInstance.beers.index(of: beer) else {
return
}
// 1.
BeerManager.sharedInstance.beers.remove(at: index)
BeerManager.sharedInstance.saveBeers()
// 2
tableView.reloadData()
updateBeerCountLabel()
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
if BeerManager.sharedInstance.beers.count == 0 {
selectedBeer = nil
}
}
Once again, very straightforward code:
- If a beer is selected, you remove it from the model.
- Reload the table view, and select the first available beer.
Handling Images
Remember how Image Wells have the ability to accept an image dropped on them? Change imageChanged(_:)
to:
@IBAction func imageChanged(_ sender: NSImageView) {
guard let image = sender.image else { return }
selectedBeer?.saveImage(image)
}
And you thought it was going to be hard! Apple has taken care of all the heavy lifting for you, and provides you with the image dropped.
On the flip side to that, you’ll need to do a bit more work to handle user’s picking the image from within your app. Replace selectImage()
with:
@IBAction func selectImage(_ sender: Any) {
guard let window = view.window else { return }
// 1.
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = true
// 2.
openPanel.allowedFileTypes = ["jpg", "png", "tiff"]
// 3.
openPanel.beginSheetModal(for: window) { (result) in
if result == NSApplication.ModalResponse.OK {
// 4.
if let panelURL = openPanel.url,
let beerImage = NSImage(contentsOf: panelURL) {
self.selectedBeer?.saveImage(beerImage)
self.imageView.image = beerImage
}
}
}
}
The above code is how you use NSOpenPanel
to select a file. Here’s what’s happening:
- You create an
NSOpenPanel
, and configure its settings. - In order to allow the user to choose only pictures, you set the allowed file types to your preferred image formats.
- Present the sheet to the user.
- Save the image if the user selected one.
Finally, add the code that will save the data model in updateBeer(_:)
:
@IBAction func updateBeer(_ sender: Any) {
// 1.
guard let beer = selectedBeer,
let index = BeerManager.sharedInstance.beers.index(of: beer) else { return }
beer.name = nameField.stringValue
beer.rating = ratingIndicator.integerValue
beer.note = noteView.string
// 2.
let indexSet = IndexSet(integer: index)
tableView.beginUpdates()
tableView.reloadData(forRowIndexes: indexSet, columnIndexes: IndexSet(integer: 0))
tableView.endUpdates()
// 3.
BeerManager.sharedInstance.saveBeers()
}
Here’s what you added:
- You ensure the beer exists, and update its properties.
- Update the table view to reflect any names changes in the table.
- Save the data to the disk.
You’re all set! Build and run the app, and start adding beers. Remember, you’ll need to select Update to save your data.