Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-09-22

Creating a NSScrollView with variable size content

From time to time I have to create a NSScrollView with a custom content NSView that changes its size, like a text editor. In addition, the "canvas size" should appear unlimited in horizontal and vertical direction. Now, this is not very difficult, but it routinely catches me off-guard and I spend more time than I should on getting the fundamentals right. To my defence, Apple has changed the general approach a couple of times ... but enough excuses. In this post I want to detail the steps to be taken such that I save myself some effort the next time I have to do this...

Step 1: Create a xib file with a NSWindow in it, this window is the "document window".

Step 2: Drag and drop a NSView into the view of the window.

Step 3: Select the inner NSView and choose (Xcode 7) menu item "Editor -> Embed in -> Scroll View". This step is important, it sets up internals such that the inner NSView is connected correctly to the NSScrollView. Do not try to do this manually by dragging and dropping a NSScrollView into the window!

Step 4: Create your document view, I will call it LineView. This class inherits from NSView. This line view will need access to the document, for this I will add a reference to it (the document is created in step 7):

    var document: LineDocument! {

Step 5: In the xib file, select the inner NSView (inside the scrollview) and open the identity inspector. Now change the class of the NSView to your chosen document view (in my case: LineView).

Step 6: Also in the xib file, add constraints to LineView to position and size it. Do the same for the scrollview inside its superview. By default I add four constraints such that each view completely fills its superview.

The above will set up a functioning scrollview. But since I want to open a variable size document in it, here are the next things to do.

Step 7: Create your own document class by creating a new class (LineDocument) and subclass from NSDocument.

Step 8: Create a document window controller, inherit from NSWindowController. The window controller (LineDocumentWindowController) will be used to give the LineView inside the scrollview access to its line document object. In order to do so add the following to the implementation to the window controller:

    @IBOutlet weak var lineView: LineView!
    
    override func windowDidLoad() {
        lineView.document = document as? LineDocument

    }

Step: 9: In the xib file, change the class of the File Owner (using the identity inspector) to the new window controller.

Step 10: In the xib file, connect the lineView outlet in the file owner to the LineView in the scroll view.

Step 11: In our document (LineDocument) add the following code to create the document window:

    override func makeWindowControllers() {
        self.addWindowController(LineDocumentWindowController(windowNibName: "LineDocument"))
    }

Note: this assumes that the xib file is called "LineDocument.xib"

Of course you will need to add code to your document to manage the data that makes up the document.

And of course you will need to add code to your document view to actually create the view content (in drawRect).

There is however one more thing to do: if the users adds content to the document you will need to update the frame of the document view. This is best done by a notification, raised by the document, observed by the view.

So far so good, but if you want a background pattern (for example alternating line background colours) another problem will crop up: when the scrollview is sized to be bigger than necessary for the document, the background pattern will only be drawn directly behind the document content and not in the surrounding space.
To fix this it is necessary to artificially change the size of the frame of the document view to include the "empty" space in the document view.
For this, I use a function "adjustFrame" which is called in drawRect. A better solution would be to call that function only when the window is resized and when the size of the document changes. But simply including it in drawRect will suffice.

The adjustFrame function in the document view class looks as follows:

    /// Resizes the frame to always fill out the available space. This ensures that the whole frame in NSScrollView is always filled out with the background pattern, giving the illusion of an endlessly big canvas.

    private func adjustFrame() {
        
        // Determine the minimum size needed for our content area, then see if the contentview is bigger. When either width or height of the contentview area is bigger, use those values.
        
        let neededFrame = minimumLinesRectangle
        let availableSize = enclosingScrollView!.contentSize
        var newFrame = frame
        
        newFrame.size.width = max(neededFrame.size.width, availableSize.width)
        newFrame.size.height = max(neededFrame.size.height, availableSize.height)
        
        frame = newFrame
    }

PS: The minimumLinesRectangle is a property that evaluates the minimum size needed to display all of the document data.

Update 2015.10.01: I still have problems where the scrollview sometimes jumps back to the upper-left corner of the documentView. I finally solved that by creating the scrollview programatically. Seems there is a difference between interface builder and programatically created scrollview hierarchies. Go figure. You can read about the programatic example here.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

2 comments:

  1. can you plz make a video of it

    ReplyDelete
  2. Sorry, I don't do video's. (We all have to draw a line somewhere ;-))

    ReplyDelete