NSOutlineView on macOS Tutorial
Discover how to display and interact with hierarchical data on macOS with this NSOutlineView on macOS tutorial. By Jean-Pierre Distler.
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
NSOutlineView on macOS Tutorial
25 mins
Introducing NSOutlineViewDataSource
So far, you’ve told the outline view that ViewController is its data source — but ViewController doesn’t yet know about its new job. It’s time to change this and get rid of that pesky error message.
Add the following extension below your class declaration of ViewController:
extension ViewController: NSOutlineViewDataSource {
}
This makes ViewController adopt the NSOutlineViewDataSource protocol. Since we’re not using bindings in this tutorial, you must implement a few methods to fill the outline view. Let’s go through each method.
Your outline view needs to know how many items it should show. For this, use the method outlineView(_: numberOfChildrenOfItem:) -> Int.
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
//1
if let feed = item as? Feed {
return feed.children.count
}
//2
return feeds.count
}
This method will be called for every level of the hierarchy displayed in the outline view. Since you only have 2 levels in your outline view, the implementation is pretty straightforward:
- If
itemis aFeed, it returns the number ofchildren. - Otherwise, it returns the number of
feeds.
One thing to note: item is an optional, and will be nil for the root objects of your data model. In this case, it will be nil for Feed; otherwise it will contain the parent of the object. For FeedItem objects, item will be a Feed.
Onward! The outline view needs to know which child it should show for a given parent and index. The code for this is similiar to the previous code:
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let feed = item as? Feed {
return feed.children[index]
}
return feeds[index]
}
This checks whether item is a Feed; if so, it returns the FeedItem for the given index. Otherwise, it return a Feed. Again, item will be nil for the root object.
One great feature of NSOutlineView is that it can collapse items. First, however, you have to tell it which items can be collapsed or expanded. Add the following:
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let feed = item as? Feed {
return feed.children.count > 0
}
return false
}
In this application only Feeds can be expanded and collapsed, and only if they have children. This checks whether item is a Feed and if so, returns whether the child count of Feed is greater than 0. For every other item, it just returns false.
Run your application. Hooray! The error message is gone, and the outline view is populated. But wait — you only see 2 triangles indicating that you can expand the row. If you click one, more invisible entries appear.
Did you do something wrong? Nope — you just need one more method.
Introducing NSOutlineViewDelegate
The outline view asks its delegate for the view it should show for a specific entry. However, you haven’t implemented any delegate methods yet — time to add conformance to NSOutlineViewDelegate.
Add another extension to your ViewController in ViewController.swift:
extension ViewController: NSOutlineViewDelegate {
}
The next method is a bit more complex, since the outline view should show different views for Feeds and FeedItems. Let’s put it together piece by piece.
First, add the method body to the extension.
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
var view: NSTableCellView?
// More code here
return view
}
Right now this method returns nil for every item. In the next step you start to return a view for a Feed. Add this code above the // More code here comment:
//1
if let feed = item as? Feed {
//2
view = outlineView.make(withIdentifier: "FeedCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
//3
textField.stringValue = feed.name
textField.sizeToFit()
}
}
This code:
- Checks if
itemis aFeed. - Gets a view for a
Feedfrom the outline view. A normalNSTableViewCellcontains a text field. - Sets the text field’s text to the feed’s name and calls
sizeToFit(). This causes the text field to recalculate its frame so the contents fit inside.
Run your project. While you can see cells for a Feed, if you expand one you still see nothing.
This is because you’ve only provided views for the cells that represent a Feed. To change this, move on to the next step! Still in ViewController.swift, add the following property below the feeds property:
let dateFormatter = DateFormatter()
Change viewDidLoad() by adding the following line after super.viewDidLoad():
dateFormatter.dateStyle = .short
This adds an NSDateformatter that will be used to create a nice formatted date from the publishingDate of a FeedItem.
Return to outlineView(_:viewForTableColumn:item:) and add an else-if clause to if let feed = item as? Feed:
else if let feedItem = item as? FeedItem {
//1
if tableColumn?.identifier == "DateColumn" {
//2
view = outlineView.make(withIdentifier: "DateCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
//3
textField.stringValue = dateFormatter.string(from: feedItem.publishingDate)
textField.sizeToFit()
}
} else {
//4
view = outlineView.make(withIdentifier: "FeedItemCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
//5
textField.stringValue = feedItem.title
textField.sizeToFit()
}
}
}
This is what you’re doing here:
- If
itemis aFeedItem, you fill two columns: one for thetitleand another one for thepublishingDate. You can differentiate the columns with theiridentifier. - If the
identifieris dateColumn, you request a DateCell. - You use the date formatter to create a string from the
publishingDate. - If it is not a dateColumn, you need a cell for a
FeedItem. - You set the text to the
titleof theFeedItem.
Run your project again to see feeds filled properly with articles.
There’s one problem left — the date column for a Feed shows a static text. To fix this, change the content of the if let feed = item as? Feed if statement to:
if tableColumn?.identifier == "DateColumn" {
view = outlineView.make(withIdentifier: "DateCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = ""
textField.sizeToFit()
}
} else {
view = outlineView.make(withIdentifier: "FeedCell", owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = feed.name
textField.sizeToFit()
}
}
To complete this app, after you select an entry the web view should show the corresponding article. How can you do that? Luckily, the following delegate method can be used to check whether something was selected or if the selection changed.
func outlineViewSelectionDidChange(_ notification: Notification) {
//1
guard let outlineView = notification.object as? NSOutlineView else {
return
}
//2
let selectedIndex = outlineView.selectedRow
if let feedItem = outlineView.item(atRow: selectedIndex) as? FeedItem {
//3
let url = URL(string: feedItem.url)
//4
if let url = url {
//5
self.webView.mainFrame.load(URLRequest(url: url))
}
}
}
This code:
- Checks if the notification object is an NSOutlineView. If not, return early.
- Gets the selected index and checks if the selected row contains a
FeedItemor aFeed. - If a
FeedItemwas selected, creates aNSURLfrom theurlproperty of theFeedobject. - Checks whether this succeeded.
- Finally, loads the page.
Before you test this out, return to the Info.plist file. Add a new Entry called App Transport Security Settings and make it a Dictionary if Xcode didn’t. Add one entry, Allow Arbitrary Loads of type Boolean, and set it to YES.
Note: Adding this entry to your plist causes your application to accept insecure connections to every host, which can be a security risk. Usually it is better to add Exception Domains to this entry or, even better, to use backends that use an encrypted connection.
Note: Adding this entry to your plist causes your application to accept insecure connections to every host, which can be a security risk. Usually it is better to add Exception Domains to this entry or, even better, to use backends that use an encrypted connection.
Now build your project and select a FeedItem. Assuming you have a working internet connection, the article will load after a few seconds.


