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.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. setFieldsEnabled(_:) will allow you to easily turn off and on the ability to use the form controls.
  2. updateBeerCountLabel() simply sets the count of beers in the beerCountField.

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:

  1. Creating a new beer.
  2. Inserting the beer into the model.
  3. Inserting a new row into the table.
  4. 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:

  1. If a beer is selected, you remove it from the model.
  2. 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:

  1. You create an NSOpenPanel, and configure its settings.
  2. In order to allow the user to choose only pictures, you set the allowed file types to your preferred image formats.
  3. Present the sheet to the user.
  4. 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:

  1. You ensure the beer exists, and update its properties.
  2. Update the table view to reflect any names changes in the table.
  3. 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.

Final UI with some beers added.