NSView:setFrameOrigin 和 setFrameSize - 是什么导致此代码中出现不一致的行为?
NSView: setFrameOrigin and setFrameSize - what is causing inconsistent behaviour in this code?
我创建了一个简化的示例。我想如果有经验的人运行这个,他们将很容易能够解释这种行为......
在示例中,每次按下 addView 按钮都会创建一个新的子子视图。每次生成一个子视图时,这些子视图都应该相同,但只有在生成第三个子视图时,我才能得到正确的结果。这是什么原因?
如果您在 drawRect 方法中取消注释 frame 和 bounds 调用的重复,您将在第二次尝试时得到正确的结果 - 同样,我无法解释这种行为。
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)
}
}
您的两个视图都没有布局限制。您不能使用坐标定位您的视图。
你应该看看自动布局。
当您使用自动布局时,一切都由视图的固有内容大小以及内容拥抱和抗压缩优先级控制。
因此您需要覆盖视图的固有内容大小,并为具有相对于父视图的布局约束的子视图使用自动布局。
您还应该删除 drawRect 中试图控制视图的位置或 width/height 的所有代码。
只在drawRect方法中绘制。
目前 AppKit 正在自动为您添加不明确的自动布局约束。它没有足够的信息来正确放置它们。这就是您所看到的。
AppKit 不希望您在其 draw(_:)
方法中更改视图的框架。
当您点击添加子视图的按钮时,这是一个事件。 AppKit 像这样处理每个事件,按此顺序:
调用事件的回调。在这种情况下,回调是您的 addView(_:)
方法。
在任何设置了 needsUpdateConstraints
的视图上调用 updateConstraints
。请注意,newly-created 视图会自动设置 needsUpdateConstraints
.
如果视图需要对自动布局约束进行大量更改,在 updateConstraints
中执行比在事件回调中执行更有效。如果您只是更改少量约束,请不要担心。
在任何设置了 needsLayout
的视图上调用 layout
。请注意,newly-created 视图会自动设置 needsLayout
.
当 AppKit 调用 layout
时,它已经更新了接收视图的框架。接收视图的 layout
方法负责更新视图子视图的框架,而不是视图本身的框架。 layout
的 NSView
实现根据自动布局约束更新子视图的框架,因此如果您覆盖 layout
方法,调用 super.layout()
通常很重要。
在已设置 needsDisplay
或因调用 setNeedsDisplay(_:)
而具有任何无效区域的任何视图上调用 draw(_:)
。请注意,newly-created 视图会自动将其整个边界标记为无效。
当 AppKit 调用 draw(_:)
时,它已经更新了正在绘制的视图的框架。
对 updateConstraints
的所有调用都在对 layout
的任何调用之前进行,对 layout
的所有调用都在对 draw(_:)
的任何调用之前进行。当 AppKit 调用 draw(_:)
时,它希望您的视图具有正确的框架,并且不希望这些框架发生变化,直到另一个事件发生。
所以您的代码中的问题是您没有在 AppKit 希望您更新每个 ChildView
的框架。您应该在 layout
中更新它,但您在 draw(_:)
中更新它。
关于您的代码的另一个奇怪之处是每个 ChildView
创建自己的 ViewController
实例,只是为了访问控制器的 yMax
属性。我想你可能想要制作 yMax
属性 static
这样你就可以在没有实例的情况下访问它,或者你希望每个 ChildView
访问现有的 ViewController
.但由于布置每个 ChildView
确实是 ParentView
的工作,因此 ParentView
需要对现有 ViewController
.
的引用
以下是我编写代码的方式:
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)
}
}
更新
您发表评论:
I am calculating geometries in draw and I use setFrameOrigin
and setFrameSize
to adjust the frame if needed depending on what needs to be drawn. I guess it would be possible to calculate the geometry of the 'drawing' before an instance of ChildView
is created and then instantiate with the correct frame. what a ChildView
needs to draw would not actually be in the ChildView
class. Any advice on this chicken and egg problem?”
根据需要计算该信息并将其存储在 ChildView
中。
假设我们想要 ChildView
绘制一个正多边形。我们要指定多边形的边数和外接半径,然后布置 ChildView
以完美适合多边形。这是一种方法:
import Cocoa
class ViewController: NSViewController {
let parentView = ParentView(frame: NSRect(x: 100, y: 10, width: 100, height: 400))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(parentView)
}
@IBAction func addView(_ sender: Any) {
let childView = ChildView(
sideCount: parentView.subviews.count + 3,
radius: 30
)
parentView.addSubview(childView)
parentView.needsLayout = true
}
}
class ParentView: NSView {
override func layout() {
super.layout()
var y = bounds.size.height
for subview in subviews.lazy.compactMap({ [=11=] as? ChildView }) {
let size = subview.intrinsicContentSize
y -= size.height
subview.frame = CGRect(x: 0, y: y, width: size.width, height: size.height)
}
}
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 {
// Here is the raw data on which my appearance depends.
var sideCount: Int { didSet { invalidate() } }
var radius: CGFloat { didSet { invalidate() } }
// Here is cached layout information that depends on my raw data.
private var detailCache: Detail? = nil
private struct Detail {
var size: CGSize
var vertices: [CGPoint]
}
init(sideCount: Int, radius: CGFloat) {
self.sideCount = sideCount
self.radius = radius
super.init(frame: .zero)
}
required init?(coder: NSCoder) { fatalError() }
override var intrinsicContentSize: CGSize { detail.size }
override func draw(_ dirtyRect: NSRect) {
let vertices = detail.vertices
guard let first = vertices.first else { return }
let path = NSBezierPath()
path.move(to: first)
for p in vertices.dropFirst() {
path.line(to: p)
}
path.close()
NSColor.green.set()
path.fill()
}
private func invalidate() {
detailCache = nil
invalidateIntrinsicContentSize()
superview?.needsLayout = true
needsDisplay = true
}
private var detail: Detail {
if let detail = detailCache { return detail }
let detail = makeDetail()
detailCache = detail
return detail
}
private func makeDetail() -> Detail {
guard sideCount > 0 else { return Detail(size: .zero, vertices: []) }
var vertices: [CGPoint] = []
var pMin = CGPoint(x: CGFloat.infinity, y: .infinity)
var pMax = CGPoint(x: -CGFloat.infinity, y: -.infinity)
for i in (0 ..< sideCount).lazy.map({ CGFloat([=11=]) }) {
let p = CGPoint(
x: radius * cos(2 * .pi * i / CGFloat(sideCount)),
y: radius * sin(2 * .pi * i / CGFloat(sideCount))
)
vertices.append(p)
pMin.x = min(pMin.x, p.x)
pMin.y = min(pMin.y, p.y)
pMax.x = max(pMax.x, p.x)
pMax.y = max(pMax.y, p.y)
}
for i in vertices.indices {
vertices[i].x -= pMin.x
vertices[i].y -= pMin.y
}
return Detail(
size: CGSize(
width: pMax.x - pMin.x,
height: pMax.y - pMin.y
),
vertices: vertices
)
}
}
我创建了一个简化的示例。我想如果有经验的人运行这个,他们将很容易能够解释这种行为......
在示例中,每次按下 addView 按钮都会创建一个新的子子视图。每次生成一个子视图时,这些子视图都应该相同,但只有在生成第三个子视图时,我才能得到正确的结果。这是什么原因?
如果您在 drawRect 方法中取消注释 frame 和 bounds 调用的重复,您将在第二次尝试时得到正确的结果 - 同样,我无法解释这种行为。
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)
}
}
您的两个视图都没有布局限制。您不能使用坐标定位您的视图。
你应该看看自动布局。
当您使用自动布局时,一切都由视图的固有内容大小以及内容拥抱和抗压缩优先级控制。
因此您需要覆盖视图的固有内容大小,并为具有相对于父视图的布局约束的子视图使用自动布局。 您还应该删除 drawRect 中试图控制视图的位置或 width/height 的所有代码。
只在drawRect方法中绘制。
目前 AppKit 正在自动为您添加不明确的自动布局约束。它没有足够的信息来正确放置它们。这就是您所看到的。
AppKit 不希望您在其 draw(_:)
方法中更改视图的框架。
当您点击添加子视图的按钮时,这是一个事件。 AppKit 像这样处理每个事件,按此顺序:
调用事件的回调。在这种情况下,回调是您的
addView(_:)
方法。在任何设置了
needsUpdateConstraints
的视图上调用updateConstraints
。请注意,newly-created 视图会自动设置needsUpdateConstraints
.如果视图需要对自动布局约束进行大量更改,在
updateConstraints
中执行比在事件回调中执行更有效。如果您只是更改少量约束,请不要担心。在任何设置了
needsLayout
的视图上调用layout
。请注意,newly-created 视图会自动设置needsLayout
.当 AppKit 调用
layout
时,它已经更新了接收视图的框架。接收视图的layout
方法负责更新视图子视图的框架,而不是视图本身的框架。layout
的NSView
实现根据自动布局约束更新子视图的框架,因此如果您覆盖layout
方法,调用super.layout()
通常很重要。在已设置
needsDisplay
或因调用setNeedsDisplay(_:)
而具有任何无效区域的任何视图上调用draw(_:)
。请注意,newly-created 视图会自动将其整个边界标记为无效。当 AppKit 调用
draw(_:)
时,它已经更新了正在绘制的视图的框架。
对 updateConstraints
的所有调用都在对 layout
的任何调用之前进行,对 layout
的所有调用都在对 draw(_:)
的任何调用之前进行。当 AppKit 调用 draw(_:)
时,它希望您的视图具有正确的框架,并且不希望这些框架发生变化,直到另一个事件发生。
所以您的代码中的问题是您没有在 AppKit 希望您更新每个 ChildView
的框架。您应该在 layout
中更新它,但您在 draw(_:)
中更新它。
关于您的代码的另一个奇怪之处是每个 ChildView
创建自己的 ViewController
实例,只是为了访问控制器的 yMax
属性。我想你可能想要制作 yMax
属性 static
这样你就可以在没有实例的情况下访问它,或者你希望每个 ChildView
访问现有的 ViewController
.但由于布置每个 ChildView
确实是 ParentView
的工作,因此 ParentView
需要对现有 ViewController
.
以下是我编写代码的方式:
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)
}
}
更新
您发表评论:
I am calculating geometries in draw and I use
setFrameOrigin
andsetFrameSize
to adjust the frame if needed depending on what needs to be drawn. I guess it would be possible to calculate the geometry of the 'drawing' before an instance ofChildView
is created and then instantiate with the correct frame. what aChildView
needs to draw would not actually be in theChildView
class. Any advice on this chicken and egg problem?”
根据需要计算该信息并将其存储在 ChildView
中。
假设我们想要 ChildView
绘制一个正多边形。我们要指定多边形的边数和外接半径,然后布置 ChildView
以完美适合多边形。这是一种方法:
import Cocoa
class ViewController: NSViewController {
let parentView = ParentView(frame: NSRect(x: 100, y: 10, width: 100, height: 400))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(parentView)
}
@IBAction func addView(_ sender: Any) {
let childView = ChildView(
sideCount: parentView.subviews.count + 3,
radius: 30
)
parentView.addSubview(childView)
parentView.needsLayout = true
}
}
class ParentView: NSView {
override func layout() {
super.layout()
var y = bounds.size.height
for subview in subviews.lazy.compactMap({ [=11=] as? ChildView }) {
let size = subview.intrinsicContentSize
y -= size.height
subview.frame = CGRect(x: 0, y: y, width: size.width, height: size.height)
}
}
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 {
// Here is the raw data on which my appearance depends.
var sideCount: Int { didSet { invalidate() } }
var radius: CGFloat { didSet { invalidate() } }
// Here is cached layout information that depends on my raw data.
private var detailCache: Detail? = nil
private struct Detail {
var size: CGSize
var vertices: [CGPoint]
}
init(sideCount: Int, radius: CGFloat) {
self.sideCount = sideCount
self.radius = radius
super.init(frame: .zero)
}
required init?(coder: NSCoder) { fatalError() }
override var intrinsicContentSize: CGSize { detail.size }
override func draw(_ dirtyRect: NSRect) {
let vertices = detail.vertices
guard let first = vertices.first else { return }
let path = NSBezierPath()
path.move(to: first)
for p in vertices.dropFirst() {
path.line(to: p)
}
path.close()
NSColor.green.set()
path.fill()
}
private func invalidate() {
detailCache = nil
invalidateIntrinsicContentSize()
superview?.needsLayout = true
needsDisplay = true
}
private var detail: Detail {
if let detail = detailCache { return detail }
let detail = makeDetail()
detailCache = detail
return detail
}
private func makeDetail() -> Detail {
guard sideCount > 0 else { return Detail(size: .zero, vertices: []) }
var vertices: [CGPoint] = []
var pMin = CGPoint(x: CGFloat.infinity, y: .infinity)
var pMax = CGPoint(x: -CGFloat.infinity, y: -.infinity)
for i in (0 ..< sideCount).lazy.map({ CGFloat([=11=]) }) {
let p = CGPoint(
x: radius * cos(2 * .pi * i / CGFloat(sideCount)),
y: radius * sin(2 * .pi * i / CGFloat(sideCount))
)
vertices.append(p)
pMin.x = min(pMin.x, p.x)
pMin.y = min(pMin.y, p.y)
pMax.x = max(pMax.x, p.x)
pMax.y = max(pMax.y, p.y)
}
for i in vertices.indices {
vertices[i].x -= pMin.x
vertices[i].y -= pMin.y
}
return Detail(
size: CGSize(
width: pMax.x - pMin.x,
height: pMax.y - pMin.y
),
vertices: vertices
)
}
}