Home > Net >  NSView: setFrameOrigin and setFrameSize - what is causing inconsistent behaviour in this code?
NSView: setFrameOrigin and setFrameSize - what is causing inconsistent behaviour in this code?

Time:01-27

I have created a simplified example. I imagine if someone experienced runs this they will easily be able to explain this behaviour...

In the example the addView button creates a new child subview each time it is pressed. These subviews should be the same each time one is generated but it is only when the third subview has been generated that I get the correct result. What is the reason for this?

If you uncomment the repetition of the frame and bounds calls in the drawRect method you get the correct result on the second attempt - again I cannot explain this behaviour.

import Cocoa

class ViewController: NSViewController {

    let parentView = ParentView(frame: NSRect(x: 100, y: 10, width: 100, height: 400))
    let yMax = 400
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(parentView)
    }

    @IBAction func addView(_ sender: Any) {
        
        let childView = ChildView(frame: NSRect(x:0, y: 0, width: 20, height: 20))
        self.parentView.addSubview(childView)
        
        for v in parentView.subviews {
            print("subview frame: ", v.frame)
        }
        
    }
    
}

class ParentView: NSView {
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        
        guard let context = NSGraphicsContext.current?.cgContext else {
            return
        }
        drawRect(ctx: context)
    }
    
    func drawRect(ctx: CGContext) {
        let parentFrameMarker = NSRect(x: 0, y: 0, width: 100, height: 400)
        ctx.addRect(parentFrameMarker)
        ctx.setStrokeColor(CGColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0))
        ctx.setLineWidth(1.0)
        ctx.strokePath()
    }
    
}

class ChildView: NSView {
    
    let controller = ViewController()
    
    
    override func draw(_ dirtyRect: NSRect) {
        
        guard let context = NSGraphicsContext.current?.cgContext else {
            return
        }
        
        let y = controller.yMax
        resizeView(y: y)
        drawRect(ctx: context, y: y)
        
    }
    
    func drawRect(ctx: CGContext, y: Int) {
        let width = 20
        let height = 40
//      let currentY = y - height
//
//      let origin = CGPoint(x: 0, y: currentY)
//      let size = NSSize(width: width, height: height)
//      self.setBoundsSize(size)
//      self.setFrameSize(size)
//      print("new frame: ", self.frame)
//      print("new bounds: ", self.bounds)
//      self.setFrameOrigin(origin)
        
        let childFrameMarker = NSRect(x:0, y: 0, width: width, height: height)
        print("childFrameMarker size: ", childFrameMarker.size)
        ctx.addRect(childFrameMarker)
        ctx.setStrokeColor(CGColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0))
        ctx.setLineWidth(2.0)
        ctx.strokePath()
        
    }
    
    func resizeView(y: Int) {
        let width = 20
        let height = 40
        let currentY = y - height
        let origin = CGPoint(x: 0, y: currentY)
        let size = NSSize(width: width, height: height)
        self.setBoundsSize(size)
        self.setFrameSize(size)
        self.setFrameOrigin(origin)
        print("new frame: ", self.frame)
        print("new bounds: ", self.bounds)
    }
    
}

CodePudding user response:

There are no layout constraints on either of your views. You cannot position your views with coordinates.

You should look into autolayout.

When you are using autolayout, everything is controlled by the intrinsic content size of the view and the content hugging and compression resistance priority.

So you need to override the intrinsic content size of the view and use autolayout for the child view with layout constraints in relation to the parent view. You should also remove all code in your drawRect, that tries to control the position or width/height of the view.

Only do drawing in the drawRect method.

Currently AppKit is adding ambiguous autolayout constraints for you automatically. It doesn't have enough information to place them correctly. That is what you are seeing.

CodePudding user response:

AppKit does not expect you to change a view's frame inside its draw(_:) method.

When you click the button to add a subview, that's an event. AppKit handles each event like this, in this order:

  1. Call the callback for the event. In this case, the callback is your addView(_:) method.

  2. Call updateConstraints on any view that has needsUpdateConstraints set. Note that a newly-created view automatically sets needsUpdateConstraints.

    If a view needs to make a bunch of changes to autolayout constraints, it's a little more efficient to do it in updateConstraints than in the event callback. If you're only changing a small number of constraints, don't worry about it.

  3. Call layout on any view that has needsLayout set. Note that a newly-created view automatically sets needsLayout.

    By the time AppKit calls layout, it has already updated the frame of the receiving view. The receiving view's layout method is responsible for updating the frames of the view's subviews, but not of the view itself. The NSView implementation of layout updates the frames of subviews based on autolayout constraints, so it's generally important to call super.layout() if you override the layout method.

  4. Call draw(_:) on any view that has needsDisplay set or that has any invalid regions due to calls to setNeedsDisplay(_:). Note that a newly-created view automatically marks its entire bounds as invalid.

    By the time AppKit calls draw(_:), it has already updated the frame of the view being drawn.

All calls to updateConstraints are made before any calls to layout, and all calls to layout are made before any calls to draw(_:). By the time AppKit calls draw(_:), it expects your views to have their correct frames and doesn't expect those frames to change until another event happens.

So the problem in your code is that you're not updating the frame of each ChildView when AppKit expects you to. You should be updating it in layout, but you're updating it in draw(_:).

Another weird thing about your code is that each ChildView creates its own instance of ViewController, just to access the controller's yMax property. I think you probably want to either make the yMax property static so you can access it without an instance, or you want each ChildView to access the existing ViewController. But since it's really ParentView's job to lay out each ChildView, it is ParentView that needs a reference to the existing ViewController.

Here's how I might write your code:

import Cocoa

class ViewController: NSViewController {

    let parentView = ParentView(frame: NSRect(x: 100, y: 10, width: 100, height: 400))
    let yMax = 400

    override func viewDidLoad() {
        super.viewDidLoad()
        parentView.controller = self
        self.view.addSubview(parentView)
    }

    @IBAction func addView(_ sender: Any) {
        let childView = ChildView(frame: NSRect(x:0, y: 0, width: 20, height: 20))
        parentView.addSubview(childView)
        parentView.needsLayout = true
    }
}

class ParentView: NSView {

    weak var controller: ViewController?

    override func layout() {
        super.layout()

        guard var y = controller?.yMax else { return }

        for subview in subviews {
            y -= 40
            subview.frame = CGRect(x: 0, y: y, width: 20, height: 40)
        }
    }

    override func draw(_ dirtyRect: NSRect) {
        // No call to `super.draw(_:)` needed because ParentView is a direct subclass of NSView.
        NSColor.red.set()
        bounds.frame()
    }
}

class ChildView: NSView {
    override func draw(_ dirtyRect: NSRect) {
        NSColor.green.set()
        bounds.frame(withWidth: 2)
    }
}
  •  Tags:  
  • Related