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:
Call the callback for the event. In this case, the callback is your
addView(_:)method.Call
updateConstraintson any view that hasneedsUpdateConstraintsset. Note that a newly-created view automatically setsneedsUpdateConstraints.If a view needs to make a bunch of changes to autolayout constraints, it's a little more efficient to do it in
updateConstraintsthan in the event callback. If you're only changing a small number of constraints, don't worry about it.Call
layouton any view that hasneedsLayoutset. Note that a newly-created view automatically setsneedsLayout.By the time AppKit calls
layout, it has already updated the frame of the receiving view. The receiving view'slayoutmethod is responsible for updating the frames of the view's subviews, but not of the view itself. TheNSViewimplementation oflayoutupdates the frames of subviews based on autolayout constraints, so it's generally important to callsuper.layout()if you override thelayoutmethod.Call
draw(_:)on any view that hasneedsDisplayset or that has any invalid regions due to calls tosetNeedsDisplay(_:). 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)
}
}
