Synchronize Scrolling of Two (or More) NSScrollViews
You can make two NSScrollView
s scroll in concert quite easily because every scrolled pixel is broadcasted to interested parties.
In TableFlip, the main table is a NSTableView
contained in a NSScrollView
. You can view and hide row numbers in TableFlip; but I didn’t want to reload the whole table and mess with the table model to insert and remove the first column. Instead, I use a second table view with a single column. The upside of this approach: I can animate hiding the whole scroll view with the row numbers inside easily without affecting the main table.
Synchronizing two or more scroll views is pretty simple: upon scrolling, the NSScrollView
’s NSClipView
can post a NSView.boundsDidChangeNotification
. Simply subscribe to that.
Note that you need to enable posting the notification first: set NSView.postsBoundsChangedNotifications = true
for the NSClipView
that you want to observe.
I put the logic for this into a NSScrollView
subclass with an @IBOutlet
to the scroll view that the current one should be synced to. This way, I can wire them in Interface Builder and don’t have to write code for that.
class SynchronizedScrollView: NSScrollView {
@IBOutlet weak var sourceScrollView: NSScrollView!
lazy var notificationCenter: NotificationCenter = NotificationCenter.default
deinit {
notificationCenter.removeObserver(self)
}
override func awakeFromNib() {
super.awakeFromNib()
let scrollingView = sourceScrollView.contentView
scrollingView.postsBoundsChangedNotifications = true
notificationCenter.addObserver(self,
selector: #selector(scrollViewContentBoundsDidChange(_:)),
name: NSView.boundsDidChangeNotification,
object: scrollingView)
}
@objc func scrollViewContentBoundsDidChange(_ notification: Notification) {
guard let scrolledView = notification.object as? NSClipView else { return }
let viewToScroll = self.contentView
let currentOffset = viewToScroll.bounds.origin
var newOffset = currentOffset
newOffset.y = scrolledView.documentVisibleRect.origin.y
guard newOffset != currentOffset else { return }
viewToScroll.scroll(to: newOffset)
self.reflectScrolledClipView(viewToScroll)
}
}