Search code examples
iosswiftwatchkituicolor

Sending A UIColor From Phone To Watch Results In Weird Colors


OK. This is really weird. I'm trying to transmit colors from a phone app to its companion Watch app.

The standard (UIColor.red, etc.) colors work, but ones defined by the IB file don't. I get weird colors, which seems as if it's a color space issue.

First, here's a VERY simple (and absolutely HIDEOUS) app project that demonstrates this.

What I am doing, is sending a message from the Watch to the Phone, asking the phone for a set of colors.

The phone obliges by compiling a list of UIColor instances, and sending them over to the watch. It does this by first instantiating three standard colors (UIColor.red, UIColor.green, UIColor.blue), and then appending some colors that are read from a list of labels defined in the app storyboard (It reads the text color from the label).

The resulting array of seven UIColor instances is then serialized and sent over to the watch, which unserializes them, and creates a simple table of labels; each with the corresponding color.

The first three colors look good:

The first three (Standard) colors are golden

However, when I read the colors from the IB, they get skewed.

Here's what's supposed to happen:

These colors should be exactly matched on the Watch

Here's what actually happens:

The colors are wrong

Only the last (Odd) color is correct. The colors are defined in the storyboard as RGB (sliders).

Here's the code in the iOS app that gathers and sends the color:

func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
    print("iOS App: session(_:,didReceiveMessage:)\n\(message)")
    if let value = message["HI"] as? String {
        if "HOWAYA" == value {
            var colorArray: [UIColor] = [UIColor.red, UIColor.green, UIColor.blue]
            if let primaryViewController = self.window?.rootViewController {
                if let view = primaryViewController.view {
                    for subview in view.subviews {
                        if let label = subview as? UILabel {
                            if let color = label.textColor {
                                colorArray.append(color)
                            }
                        }
                    }
                }
            }
            let colorData = NSKeyedArchiver.archivedData(withRootObject: colorArray)
            let responseMessage = [value:colorData]

            session.sendMessage(responseMessage, replyHandler: nil, errorHandler: nil)
        }
    }
}

Here's the code in the Watch that gets the colors, and instantiates the labels:

func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
    if let value = message["HOWAYA"] as? Data {
        if let colorArray = NSKeyedUnarchiver.unarchiveObject(with:value) as? [UIColor] {
            self.labelTable.setNumberOfRows(colorArray.count, withRowType: "TestColorTableRow")
            for row in 0..<colorArray.count {
                if let controller = self.labelTable.rowController(at: row) as? TestTableRowController {
                    controller.setLabelColor(colorArray[row])
                }
            }
        }
    }
}

Other data that I send (in another app) gets through fine, but the colors are skewed.

Any ideas?

UPDATE: It was suggested that maybe the serialization was a problem, so I branched the demo, and unserialized while still in iOS. It turns out OK.

I created this branch, where I unserialize before sending the message:

func colorCrosscheck(_ value: Data) {
    if let colorArray = NSKeyedUnarchiver.unarchiveObject(with:value) as? [UIColor] {
        if let primaryViewController = self.window?.rootViewController {
            if let view = primaryViewController.view {
                for subview in view.subviews {
                    if let containerView = subview as? ViewList {
                        for row in 3..<colorArray.count {
                            if let label = containerView.subviews[row - 3] as? UILabel {
                                label.textColor = colorArray[row]
                            }
                        }
                        break
                    }
                }
            }
        }
    }
}

What I did, was add four labels with clear text color, that are colorized by the same process I use on the Watch.

A few seconds after the Watch gets its message, four labels appear below the top ones in iOS.

The results show the serialization isn't the issue:

Looks OK on iOS

UPDATE (2): It was suggested I try programmatically adding colors. These work fine, which means this is starting to look like an Apple bug. I'll report back when I get a response to my DTS incident.

First, here's a new branch that demonstrates adding the colors dynamically.

Here's the relevant section of code (I also messed with the IB file):

colorArray.append(UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 1.0))
colorArray.append(UIColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0))
colorArray.append(UIColor(red: 0.0, green: 0.0, blue: 0.5, alpha: 1.0))
colorArray.append(UIColor(hue: 1.2, saturation: 1.0, brightness: 0.5, alpha: 1.0))
colorArray.append(UIColor(hue: 1.2, saturation: 0.6, brightness: 0.7, alpha: 1.0))

I added 5 new dynamic colors, in both RGB and HSL.

They show up fine in the iOS app:

Looks Good on iOS (If you're a deranged magpie)

And they also transmit properly to the Watch:

The first Three The Last Two


Solution

  • OK. I have a workaround. It's definitely a bug, but I can't wait until iOS 11/WatchOS 4 comes out. This workaround is effective (so far -lots more testing to go).

    Here's the branch with the workaround.

    if let color = label.textColor {
        if let destColorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB) {
            let newColor = color.cgColor.converted(to: destColorSpace, intent: CGColorRenderingIntent.perceptual, options: nil)
            colorArray.append(UIColor(cgColor: newColor!))
        }
    }
    

    It looks like the core color of the IB-based color is bad. Not sure how it gets the color right.

    What I do, is create a new CGColor from the original one, casting it to sRGB. The new CGColor is valid.

    It's a yucky and kludgy fix, but it should work fine.

    I need to test for memory leaks, though. CG tends to leak like a sieve.