Solution for Ch31 Challenge: Draggable Divider


#1

Create new file NerdSplitViewController.swift

import Cocoa

class NerdSplitViewController: NSViewController {
  let minimumViewWidth: CGFloat = 100
  let minimumViewHeight: CGFloat = 100
  var firstViewWidth: CGFloat = 100
  var firstViewWidthConstraint: NSLayoutConstraint?
  var isResizing: Bool = false
  
  override func mouseDown(with event: NSEvent) {
    if abs(event.locationInWindow.x - firstViewWidth) < 10 {
      isResizing = true
    }
  }
  
  override func mouseDragged(with event: NSEvent) {
    if isResizing {
      firstViewWidth = max(minimumViewWidth, event.locationInWindow.x)
      reset()
    }
  }
  
  override func mouseUp(with event: NSEvent) {
    isResizing = false
  }
  
  func reset() {
    guard childViewControllers.count == 2 else {
      return
    }
    
    let firstView = childViewControllers[0].view
    firstView.translatesAutoresizingMaskIntoConstraints = false
    
    let separator = NSBox()
    separator.boxType = .separator
    separator.translatesAutoresizingMaskIntoConstraints = false
    
    let secondView = childViewControllers[1].view
    secondView.translatesAutoresizingMaskIntoConstraints = false

    view.subviews = [firstView, separator, secondView]
    let viewsDict = ["V0" : firstView, "separator" : separator, "V1" : secondView]
    let metricsDict = ["minWidth" : minimumViewWidth as NSNumber, "minHeight" : minimumViewHeight as NSNumber]
    
    firstViewWidthConstraint?.isActive = false
    firstViewWidthConstraint = firstView.widthAnchor.constraint(equalToConstant: firstViewWidth)
    firstViewWidthConstraint?.isActive = true

    func addVisualFormatConstraints(_ visualFormat:String) {
      let constraints = NSLayoutConstraint.constraints(withVisualFormat: visualFormat, options: [], metrics: metricsDict, views: viewsDict)
      NSLayoutConstraint.activate(constraints)
    }
    addVisualFormatConstraints("H:|[V0(>=minWidth)][separator(==1)][V1(>=minWidth)]|")
    addVisualFormatConstraints("V:|[V0]|")
    addVisualFormatConstraints("V:|[separator(>=minHeight)]|")
    addVisualFormatConstraints("V:|[V1]|")
  }
  
  override func loadView() {
    view = NSView()
    reset()
  }
  
  override func insertChildViewController(_ childViewController: NSViewController, at index: Int) {
    super.insertChildViewController(childViewController, at: index)
    if isViewLoaded {
      reset()
    }
  }
  
  override func removeChildViewController(at index: Int) {
    super.removeChildViewController(at: index)
    if isViewLoaded {
      reset()
    }
  }
}

Update AppDelegate.swift

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
  var window: NSWindow?

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    let flowViewController = ImageViewController()
    flowViewController.title = "Flow"
    flowViewController.image = NSImage(named: NSImageNameFlowViewTemplate)
    
    let columnViewController = ImageViewController()
    columnViewController.title = "Column"
    columnViewController.image = NSImage(named: NSImageNameColumnViewTemplate)
    
    let splitViewController = NerdSplitViewController()
    splitViewController.addChildViewController(flowViewController)
    splitViewController.addChildViewController(columnViewController)
    
    let window = NSWindow(contentViewController: splitViewController)
    window.makeKeyAndOrderFront(self)
    self.window = window
  }
}

#2

I came up with something a little different. The one above seemed a little simpler than mine, so I tried it and found that when dragging the divider, both views changed to the same size no matter which way the divider was dragged. With mine, when dragged left, the firstView gets smaller and the second view gets bigger and visa versa.

My AppDelegate.swift is the same as above.

NerdSplitViewController.swift:

import Cocoa

class NerdSplitViewController: NSViewController {

let maxWidth: CGFloat = 600
let minimumViewWidth: CGFloat = 50
let minimumViewHeight: CGFloat = 300
var firstViewWidth: CGFloat = 300
var secondViewWidth: CGFloat = 300
var mouseDown: CGFloat!
var originalFirstViewWidth: CGFloat = 300
var firstViewWidthConstraint: NSLayoutConstraint?
var secondViewWidthConstraint: NSLayoutConstraint?
var isResizing: Bool = false

func reset() {
	guard childViewControllers.count == 2 else {
		return
	}
	
	let firstView = childViewControllers[0].view
	firstView.translatesAutoresizingMaskIntoConstraints = false
	
	let separator = NSBox()
	separator.boxType = .separator
	separator.translatesAutoresizingMaskIntoConstraints = false
	
	let secondView = childViewControllers[1].view
	secondView.translatesAutoresizingMaskIntoConstraints = false
	
	view.subviews = [firstView, separator, secondView]
	let viewsDict = ["V0" : firstView, "separator" : separator, "V1" : secondView]
	let metricsDict = ["minWidth" : minimumViewWidth as NSNumber,
					   "minHeight" : minimumViewHeight as NSNumber]
	
	// Sets the new width of the first view
	firstViewWidthConstraint?.isActive = false
	firstViewWidthConstraint = firstView.widthAnchor.constraint(equalToConstant: firstViewWidth)
	firstViewWidthConstraint?.isActive = true
	
	// sets the new width of hte second view
	secondViewWidthConstraint?.isActive = false
	secondViewWidthConstraint = secondView.widthAnchor.constraint(equalToConstant: secondViewWidth)
	secondViewWidthConstraint?.isActive = true
	
	func addVisualFormatConstraints(_ visualFormat: String) {
		let constraints = NSLayoutConstraint.constraints(withVisualFormat: visualFormat,
														options: [],
														metrics: metricsDict,
														views: viewsDict)
		NSLayoutConstraint.activate(constraints)
	}
	
	// the visual format for the second view needed to edited so as not to be equal to the first view.
	addVisualFormatConstraints("H:|[V0(>=minWidth)][separator(==1)][V1(>=minWidth)]|")
	addVisualFormatConstraints("V:|[V0]|")
	addVisualFormatConstraints("V:|[separator(>=minHeight)]|")
	addVisualFormatConstraints("V:|[V1]|")
}

override func loadView() {
		view = NSView()
		reset()
}

override func insertChildViewController(_ childViewController: NSViewController, at index: Int) {
	super.insertChildViewController(childViewController, at: index)
	if isViewLoaded {
		reset()
	}
}

override func removeChildViewController(at index: Int) {
	super.removeChildViewController(at: index)
	if isViewLoaded {
		reset()
	}
}

// MARK: - Mouse Events

override func mouseDown(with event: NSEvent) {
	
	let locationX = event.locationInWindow.x
	// This will allow clicks within +/- 5 of the separator
	if (locationX <= firstViewWidth + 5 && locationX >= firstViewWidth - 5)  {
		mouseDown = event.locationInWindow.x
		isResizing = true
	}
}

override func mouseDragged(with event: NSEvent) {
	
	if isResizing {
		
		// The firstWidthView is either the minimumViewWidth or the x location in the window, whichever is greater
		// The secondViewWidth is either the minimumViewWidth or the lesser of maxWidth or
		// 		the secondViewwidth plus the difference of x in the firstViewWindow.
		firstViewWidth = max(minimumViewWidth, event.locationInWindow.x)
		secondViewWidth = max(minimumViewWidth,
			min(maxWidth, secondViewWidth + (originalFirstViewWidth - firstViewWidth)))
		reset()
	}
}

override func mouseUp(with event: NSEvent) {
	originalFirstViewWidth = firstViewWidth
	isResizing = false
 }
}