6

This question asks whether one can use subscripting with CKRecord in Swift. While I already knew how to do what the questioner wanted, every permutation of it gives me a stack overflow:

subscript(key: String) -> CKRecordValue? {
    get {
        return objectForKey(key) as CKRecordValue?
    }
    set {
        setObject(newValue, forKey: key)
    }
}

The stack overflow occurs in the getter. (I've never tried the setter, so it may occur there, too.) I've tried implementing with objectForKey:, objectForKeyedSubscript:, and valueForKey:. All produce the same result: a stack overflow.

This is very strange, since CKRecord is certainly written in Objective-C. Why would it recursively call Swift's subscript method? It makes no sense. Nate Cook, in his answer to the questioner, wonders why Swift doesn't bridge objectForKeyedSubscript: automatically. Well, maybe the code to do that is not fully baked, but is causing this problem. I will have to try it with another class that has objectForKeyedSubscript:.

UPDATE

It appears that objectForKeyedSubscript: is ordinarily bridged. I created a class in Objective-C with the appropriate methods, added it to the bridging header, and the indexers were there and compiled without issue. Even better, it worked without a stack overflow.

This means that something very unusual is going on with CKRecord.

A THEORY

If you create a class in Swift that descends from NSObject and implements the subscript method on it with a String as the key, this becomes objectForKeyedSubscript:. (For "pure Swift" classes, I suspect this is not the case.) You can verify this by importing your Swift class into Objective-C and verifying that objectForKeyedSubscript: is there.

Since CKRecord descends from NSObject, implementing subscript overrides the default implementation. Further, it seems that objectForKey: and valueForKey: all ultimately called objectForKeyedSubscript:, which results in (read: "is the same as") a call to subscript, which causes the stack overflow.

That may explain why the stack overflow occurs. It still does not explain why objectForKeyedSubscript: was not automatically bridged, but perhaps it's because the definition of setObject:forKeyedSubscript: has a slightly different type signature from the canonical one: - (void)setObject:(id <CKRecordValue>)object forKeyedSubscript:(NSString *)key;. This makes no difference to Objective-C, but might trip up the "bridging code". Swift is pretty new, after all.

2
  • 1
    I'm not familiar with CKRecord but it sounds to me like a bug report to Apple wouldn't be a wrong move here. Commented Nov 27, 2014 at 0:01
  • Agreed. Now that I think I've figured it out, that's exactly what I'm going to do. Commented Nov 27, 2014 at 0:09

3 Answers 3

2

After some testing and debugging (via a subclass), I discovered that, for CKRecord, objectForKey: does indeed call objectForKeyedSubscript:. Also, implementing subscript in a Swift class that is marked @objc implicitly (by descending from NSObject) or explicitly means that subscript is implemented as objectForKeyedSubscript:.

This means that implementing subscript on CKRecord in an extension hides the default implementation, which causes the stack overflow.

Sign up to request clarification or add additional context in comments.

Comments

1

Here’s a simple extension to CKRecord to make it easier to subscript with.

extension CKRecord {
    struct Sub {
        let record: CKRecord

        subscript(key: String) -> CKRecordValue? {
            get {
                return record.objectForKey(key) as? CKRecordValue
            }
            set {
                record.setObject(newValue, forKey: key)
            }
        }
    }

    var sub: Sub {
        return Sub(record: self)
    }

    var 👌: Sub {
        return sub
    }
}

Usage:

var sub = record.sub
sub["name"] = name

/* Or */

// One does not simply subscript CKRecord
record.👌["name"] = name

(I’m kidding about the 👌 by the way)

Comments

1

I was able to successfully subscript by piggy-backing off of some code written by an Apple engineer on dev forums.

import CloudKit

protocol MyCKRecordValueType {
    var asObject: CKRecordValue { get }
}

extension CKRecord {
    func set<ValueType>(value: ValueType, forKey key: String) where ValueType : MyCKRecordValueType {
        let object = value.asObject
        self.setObject(object, forKey: key)
    }
    subscript(key : String) -> MyCKRecordValueType? {
        set {
            self.setObject(newValue?.asObject, forKey: key)
        }
        get {
            return object(forKey: key) as? MyCKRecordValueType
        }
    }
}

extension String : MyCKRecordValueType {
    var asObject: CKRecordValue { return self as NSString }
}
extension Bool : MyCKRecordValueType {
    var asObject: CKRecordValue { return self as NSNumber }
}
extension Int : MyCKRecordValueType {
    var asObject: CKRecordValue { return self as NSNumber }
}
extension Data : MyCKRecordValueType {
    var asObject: CKRecordValue { return self as NSData }
}

you can then call the subscript as you would expect:

let firstRecordID = CKRecordID(recordName: "0")

        let record = CKRecord(recordType: "Foo", recordID: firstRecordID)

        record["title"] = "Hello World"

        record["year_established"] = 2000

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.