PKDrawing() == PKDrawing() returns false

Currently there is no way to check if a PKCanvasView is blank. I submitted a bug via the feedback app a week ago, but it says "No similar reports." This seems like a pretty big deal, and I was wondering if anyone has been able to check a PKCanvasView to see if it's blank. The demo app uses the PKCanvasViewDelegate to flip a bool when drawing starts, but this is not really valid. If a user draws and then erases back to a blank canvas, it will still report the canvas as "dirty." I don't think saving a bunch of blank canvases is user friendly.

Accepted Reply

Let's clear up a couple of things this discussion left up in the air.


PKDrawing is an Obj-C class in Obj-C. For Swift, there is a PencilKit overlay, which causes PKDrawing to be re-defined as a struct with an inner reference to the underlying Obj-C instance. This is a fairly common bridging pattern in Swift, because Swift prefers you to use value semantics over reference semantics when possible. (The "why" to that is a longer discussion.)


If the duplication of the name "PKDrawing" seems confusing, keep in mind that a type name in Swift is qualified by its module name, so the struct's name is really PencilKit.PKDrawing. (Well, you can think of it that way, but the name is actually mangled according to Swift's name rules, so it's something uglier.) The Obj-C class name has no module qualifier. It's just plain "PKDrawing", though the PencilKit overlay forces the compiler to hide that Obj-C class name from you.


— When you use "==" on Obj-C objects in Swift, it actually calls through to the -[isEqual:] method, whose default implementation is pointer equality. IOW, for Obj-C objects in Swift, "==" will default to the equivalent of "===". However, many Obj-C objects have class-specific overrides of -[isEqual:], and in that case, Swift's "==" will typically be different from "===".


That's all just regular Obj-C behavior, expressed in Swift syntax.


— We don't know exactly how the Swift overlay for PKDrawing defines "==", and we don't know how the underlying Obj-C PKDrawing class defines -[isEqual:], so it seems a bit risky to depend on behavior that compares two PKDrawing values for equality. My guess is that Swift "==" just calls through to the underlying Obj-C PKDrawing -[isEqual:], but who knows. Even if you could figure out the current implementation, there's no API contract (AFAIK) that would ensure the same behavior forever more.


After all, is there any reliable intuition about what makes two drawings "equal"?


— There is a .null CGRect as well as a .zero CGRect. The difference is that a .zero CGRect has an origin (0,0), while .null doesn't have an origin. Forming a union of — for example — .zero and (x:1, y: 1, w: 1, h: 1) is the rect (x: 0, y: 0, w: 2, h: 2). By contrast, the union of .null and (x:1, y: 1, w: 1, h: 1) is (x:1, y: 1, w: 1, h: 1) unchanged.


Therefore, I'd expect the correct check for an "empty" drawing would be drawing.bounds.isEmpty, since that would be true for .null as well as any empty rect.

All in all, that's a whole barrel o' Swift fun from what looked like a pretty straightforward question. 🙂

Replies

Can you show in code how you test the equality ?


PKDrawing is a class, so a reference, not a value.

When you compare

PKDrawing() == PKDrawing()

- you create 2 different instances

- so their references are different

just as if you wrote

let pk1 = PKDrawing()

let pk2 = PKDrawing()

print(pk1 == pk2)

You compare the pointers, not the contents, so result is false.


That is the same with any class.

Just test:

print("Equal", UIScrollView() == UIScrollView())


yiields

Equal false

That is the same with any class.

That is not correct.

print(NSArray() == NSArray())   //->true
print(NSString() == NSString()) //->true

== should return a value based on the Equatable conformance. === checks for identity. Also PKDrawing is a struct according to my XCode (beta 6) and the developer documentation within XCode. I do see PKDrawing is listed as a class online though. I don't care so much about the implementation, I just need a way to check if a PKCanvasView is blank.

I cannot test to be sure.


But I guess you could test pkDrawing.bounds. Is empty, it should be .zero, and different if not empty…

That's my current fall back, but it seems pretty hacky. I thought it would be .zero too, but it's actually (inf, inf, 0.0, 0.0). The other idea was testing the size of the data, 42 bytes.

Surprising you get (inf, inf, 0.0, 0.0) with bounds, which is CGREct

var bounds: CGRect { get }


Anyway, to make it a bit cleaner, you could write an extension to define isBlank property.

Yes !


Even though (if I'm right), NSArray and NSMutableArray are both class, they behave differently (but just as value type for ==) :


        let x1 = NSArray()
        let x2 = NSArray()
        let y1 = NSMutableArray()
        let y2 = NSMutableArray()
        print("test x", "identical", x1 === x2, "equal", x1 == x2)
        print("test y", "identical", y1 === y2, "equal", y1 == y2)


test x identical true equal true

test y identical false equal true

Let's clear up a couple of things this discussion left up in the air.


PKDrawing is an Obj-C class in Obj-C. For Swift, there is a PencilKit overlay, which causes PKDrawing to be re-defined as a struct with an inner reference to the underlying Obj-C instance. This is a fairly common bridging pattern in Swift, because Swift prefers you to use value semantics over reference semantics when possible. (The "why" to that is a longer discussion.)


If the duplication of the name "PKDrawing" seems confusing, keep in mind that a type name in Swift is qualified by its module name, so the struct's name is really PencilKit.PKDrawing. (Well, you can think of it that way, but the name is actually mangled according to Swift's name rules, so it's something uglier.) The Obj-C class name has no module qualifier. It's just plain "PKDrawing", though the PencilKit overlay forces the compiler to hide that Obj-C class name from you.


— When you use "==" on Obj-C objects in Swift, it actually calls through to the -[isEqual:] method, whose default implementation is pointer equality. IOW, for Obj-C objects in Swift, "==" will default to the equivalent of "===". However, many Obj-C objects have class-specific overrides of -[isEqual:], and in that case, Swift's "==" will typically be different from "===".


That's all just regular Obj-C behavior, expressed in Swift syntax.


— We don't know exactly how the Swift overlay for PKDrawing defines "==", and we don't know how the underlying Obj-C PKDrawing class defines -[isEqual:], so it seems a bit risky to depend on behavior that compares two PKDrawing values for equality. My guess is that Swift "==" just calls through to the underlying Obj-C PKDrawing -[isEqual:], but who knows. Even if you could figure out the current implementation, there's no API contract (AFAIK) that would ensure the same behavior forever more.


After all, is there any reliable intuition about what makes two drawings "equal"?


— There is a .null CGRect as well as a .zero CGRect. The difference is that a .zero CGRect has an origin (0,0), while .null doesn't have an origin. Forming a union of — for example — .zero and (x:1, y: 1, w: 1, h: 1) is the rect (x: 0, y: 0, w: 2, h: 2). By contrast, the union of .null and (x:1, y: 1, w: 1, h: 1) is (x:1, y: 1, w: 1, h: 1) unchanged.


Therefore, I'd expect the correct check for an "empty" drawing would be drawing.bounds.isEmpty, since that would be true for .null as well as any empty rect.

All in all, that's a whole barrel o' Swift fun from what looked like a pretty straightforward question. 🙂

I'll keep copy of your explanation for a very subtle issue.

If I can't check if two drawings are "equal," what would be the best way to see if one drawing is exactly the same as the other? My use case is that a user can create a drawing and come back to edit it later. However, I also include undo and redo buttons, and I don't want my user to have the ability to save a change to their drawing that they undid.

As thisisnotmikey said, you can check the size of the data and see if its 42 bytes, however this also doesn't work because anytime you use the eraser, it adds to the data size. So even if you have a blank canvas and only use the erase, the data size will ballon.

canvasView.drawing.strokes.isEmpty seems to work well. So does canvasView == CGRect(origin: CGPoint(x: CGFloat.infinity, y: CGFloat.infinity), size: .zero).
As @rspoon3 mentioned.


canvasView.drawing.strokes.isEmpty 



this is working correctly.