Problems with Homekit Catalog in Swift 3

Apple's code for the HomeKit Catalog app is available for Swift 2.3 as of the 9/13/16 release. I'm trying to use some of that code (the HMCharacteristic extension) in my app, which is all in Swift 3.


Much of the HMCharacteristic extension converts to Swift 3 successfully.


The problem I'm seeing centers on the NumberFormatter. I cannot seem to get the syntax right for the conversion of a number to a string.


Here's the extension as provided by Apple for Swift 2.3. The problem is in line #40.

import HomeKit
extension HMCharacteristic {

    private struct Constants {
        static let valueFormatter = NSNumberFormatter()
        static let numericFormats = [
            HMCharacteristicMetadataFormatInt,
            HMCharacteristicMetadataFormatFloat,
            HMCharacteristicMetadataFormatUInt8,
            HMCharacteristicMetadataFormatUInt16,
            HMCharacteristicMetadataFormatUInt32,
            HMCharacteristicMetadataFormatUInt64
        ]
    }

    /*
        Returns the localized description for a provided value, taking the characteristic's metadata and possible
        values into account.

        - parameter value: The value to look up.

        - returns: A string representing the value in a localized way, e.g. `"24%"` or `"354º"`
    */
    func localizedDescriptionForValue(value: AnyObject) -> String {
        if self.isWriteOnly {
            return NSLocalizedString("Write-Only Characteristic", comment: "Write-Only Characteristic")
        }
        else if self.isBoolean {
            if let boolValue = value.boolValue {
                return boolValue ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off")
            }
        }
        if let number = value as? Int {
            if let predeterminedValueString = self.predeterminedValueDescriptionForNumber(number) {
                return predeterminedValueString
            }
    
            if let stepValue = self.metadata?.stepValue {
                Constants.valueFormatter.minimumFractionDigits = Int(log10(1.0 / stepValue.doubleValue))
                if let string = Constants.valueFormatter.stringFromNumber(number) {
                    return string + self.localizedUnitDecoration
                }
            }
        }
        return "\(value)"
    }


The line that causes trouble in Swift 3 is #03 below, where the integer "number " is to be converted to a string:


            if let stepValue = self.metadata?.stepValue {
                Constants.valueFormatter.minimumFractionDigits = Int(log10(1.0 / stepValue.doubleValue))
                if let string = Constants.valueFormatter.string(from: number) {
                    return string + self.localizedUnitDecoration
                }


I can't this to work in Playground either:

import UIKit
let valueFormatter = NumberFormatter()
let value = 23
if let myNumber = value as? Int {
    print("myNumber is \(myNumber)")   <-- "myNumber is 23"

    if let string = valueFormatter.string(from number: myNumber) {
print("myNumber is \(myNumber) and the string is \(string)")
    }
}


error: NumberFormatter testing.playground:6:49: error: cannot convert value of type 'Int' to expected argument type 'NSNumber'

if let string = valueFormatter.string(from: myNumber) {


In addition to the syntax above, I've tried the suggestion:

    if let string = valueFormatter.string(from: NSNumber(myNumber))


    if let string = valueFormatter.string(from:myNumber)


Nothing is working!

Accepted Reply

Try this:

    if let string = valueFormatter.string(from: myNumber as NSNumber)


Most implicit type conversions, like `Int` to `NSNumber`, are removed from Swift 3, and when you need such bridging conversion, you can use `as` explicitly. (Not `as?` nor `as!`.)


Or else you can construct an NSNumber with:

    if let string = valueFormatter.string(from: NSNumber(value: myNumber))

Replies

Try this:

    if let string = valueFormatter.string(from: myNumber as NSNumber)


Most implicit type conversions, like `Int` to `NSNumber`, are removed from Swift 3, and when you need such bridging conversion, you can use `as` explicitly. (Not `as?` nor `as!`.)


Or else you can construct an NSNumber with:

    if let string = valueFormatter.string(from: NSNumber(value: myNumber))

That cleared the error. Thanks!.


Now there are only a couple remaining issues. Lines 04 and 12 below cause an error "Cannot invoke initializer for type 'Bool' with an argument list of type '(Int)'"


These are the next few lines following those in the first post.


    func predeterminedValueDescriptionForNumber(number: Int) -> String? {
        switch self.characteristicType {
        case HMCharacteristicTypePowerState, HMCharacteristicTypeInputEvent, HMCharacteristicTypeOutputState:
            if Bool(number) {
                return NSLocalizedString("On", comment: "On")
            }
            else {
                return NSLocalizedString("Off", comment: "Off")
            }
       
        case HMCharacteristicTypeOutletInUse, HMCharacteristicTypeMotionDetected, HMCharacteristicTypeAdminOnlyAccess, HMCharacteristicTypeAudioFeedback, HMCharacteristicTypeObstructionDetected:
            if Bool(number) {
                return NSLocalizedString("Yes", comment: "Yes")
            }
            else {
                return NSLocalizedString("No", comment: "No")
            }
           }



Much later in the extension, this variable definition also has a type problem. Line 07 throws the error: "Result valueof type 'Int" does not conform to closure result type 'AnyObject'

[UPDATE] This error goes away by replacing [AnyObject] with [Any]


       /// - returns:  All of the possible values that this characteristic can contain.
         var allPossibleValues: [AnyObject]?
        guard self.isInteger else { return nil }
        guard let metadata = metadata, let stepValue = metadata.stepValue as? Double else { return nil }
        let choices = Array(0..<self.numberOfChoices)
        return choices.map { choice in
            Int(Double(choice) * stepValue)
        }

Lines 04 and 12 below cause an error …

I was unsure how

Bool(number)
ever worked, so I tried it on Swift 2. It only works when you import Foundation, presumably via implicit Int-to-NSNumber then explicitly NSNumber-to-Bool conversions. Yowsers!

Regardless, the correct way to do this, in both Swift 2 and Swift 3, is:

if number != 0 {
    …
}

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Woohoo! My app is working again. Thanks a million.


FWIW, I can supply the "fixed" HMCharacteristic extension to anyone that cares. I haven't tested it a lot but it did allow me to find and turn on a lamp from within my app.

I've another question regarding the HMCatalog and the new Swift 3 Syntax.


Lines 7 throws the error 'Generic parameter 'TargetValueType' could not be inferred in cast to HMCharacteristicWriteAction<_>'. I do not understand at all what happened there since HMCharacteristicWriteAction is a subclass of HMAction.


    func targetValueForCharacteristic(_ characteristic: HMCharacteristic) -> AnyObject? {
        if let value = targetValueMap.object(forKey: characteristic) {
            return value
        }
        else if let actions = actionSet?.actions {
            for action in actions {
                if let writeAction = action as? HMCharacteristicWriteAction
                    , writeAction.characteristic == characteristic {
                        return writeAction.targetValue
                }
            }
        }
        return nil
    }

Where is that code from, maybe an older version? I cannot find it in the latest HMCatalog release.

It's in ActionSetCreator.swift from the last version (2.2) - however migrated to Swift 3 by Xcode. The original code is:


    func targetValueForCharacteristic(characteristic: HMCharacteristic) -> AnyObject? {
        if let value = targetValueMap.objectForKey(characteristic) {
            return value
        }
        else if let actions = actionSet?.actions {
            for action in actions {
                if let writeAction = action as? HMCharacteristicWriteAction
                    where writeAction.characteristic == characteristic {
                        return writeAction.targetValue
                }
            }
        }


        return nil
    }

Haha, yeah, so it is. I guess if I can't master the "Find in Project" function, I'm probably not going to be of much help.


But I do have one piece of advice: You may get more attention starting your own thread.