The setup:
I'm writing an document-based app targeting OS X 10.11 using storyboards. The main window has an NSToolbar
with a 3-segment NSSegmentedControl
. When the segmented control is clicked, it should toggle the collapsed state of an NSSplitViewItem
in a horizontal or vertical NSSplitView
. The behavior I'm trying to achieve is the same as in Xcode 7 where a segmented control in the toolbar shows/hides the Navigator/Debug Area/Utilities views.
Currently the segmented control sends an action to the first responder. The action method is implemented by an NSSplitViewController
subclass, that then toggles it's NSSplitViewItem
's collapsed state.
The problem:
The issue is that the toolbar also contains an NSSearchField
. If the NSSearchField
has focus, or even if the segmented control itself has focus, clicking on the NSSegmentedControl
with the cursor does not result in the action method correctly making it's way up the responder chain to the NSSplitViewController
subclass.
Attempted solutions:
Previously I worked around this issue using notifications instead of target/action, but it ended up being too convoluted in the end. Another idea is to send the message to the window controller, which would then pass it to it's content view controller, which would pass it to the vertical split view controller that would then send the message (if needed) to the horizontal split view controller. While I know this would work, it also seemed like an ugly solution having to add code to 2 additional files that simply passed a message along, I thought this was what using the responder chain avoided.
Any insights would be grealy appreciated.
Final solution:
I realized that wiring the segmented control's action up to the first responder only makes sense if the key view context is important. In this case the segmented control should toggle the collapsed state of split view items in multiple nested split views, regardless of what the key view is.
Define an enumeration to represent areas of the split view:
enum SplitViewArea : Int {
// The raw values must match the order of the segmented control
case left, top, right
}
Define a protocol to communicate that a split view area should be toggled:
protocol SplitViewTogglable {
func toggleSplitViewItem(matching area: SplitViewArea)
}
Implement the segmented control action method in the window controller:
@IBAction func segmentedControlSelectionStateDidChange(_ sender: Any) {
guard let segmentedControl = sender as? NSSegmentedControl else { return }
guard let area = SplitViewArea(rawValue: segmentedControl.selectedSegment) else { return }
guard let togglable = contentViewController as? SplitViewTogglable else { return }
togglable.toggleSplitViewItem(matching: area)
}
Implement the SplitViewTogglable
protocol's method in the NSSplitViewController subclass:
func toggleSplitViewItem(matching area: SplitViewArea) {
switch area {
case .left:
leftSplitViewItem.isCollapsed = !leftSplitViewItem.isCollapsed
case .top:
// Nested NSSplitViewController that adopts SplitViewTogglable
if let togglable = centerSplitViewItem.viewController as? SplitViewTogglable {
togglable.toggleSplitViewItem(matching: area)
}
case .right:
rightSplitViewItem.isCollapsed = !rightSplitViewItem.isCollapsed
}
}
Is the NSSplitViewController
set as the contentViewController
of the window?
As part of the responder chain search for an action target, the window will consider its contentViewController
as a supplemental target if it responds to the action selector.
When the search field has key focus the responder chain does not go through the normal content area, and instead goes through the toolbar to the window. So the only way the NSSplitViewController could be a part of that search is to be the contentViewController
.