Florian Florian - 5 months ago 34
Swift Question

Maintain value semantics in Swift struct, which contains object reference

I have a Swift struct which contains an object for internal storage. How can I make sure the struct has value semantics?

public struct Times {
private let times = NSMutableIndexSet()

mutating func addTimeRange(openTime: Int, closeTime: Int) {
self.times.addIndexesInRange(NSRange(location: openTime, length: closeTime - openTime))
}
}

Answer

Swift 3 Update

Swift 3 includes value types for many types from the Foundation framework. There is now an IndexSet struct, which bridges to NSIndexSet. The internal implementation is similar to the solution below.

For more information on the new Foundation value types see: https://github.com/apple/swift-evolution/blob/master/proposals/0069-swift-mutability-for-foundation.md

Old approach in Swift 2

The copy-on-write approach is the right solution. However, it is not necessary to create a copy of the NSMutableIndexSet, if only one struct instance references it. Swift provides a global function called isUniquelyReferencedNonObjC() to determine if a pure Swift object is only referenced once.

Since we cannot use this function with Objective-C classes, we need to wrap NSMutableIndexSet in a Swift class.

public struct Times {
    private final class MutableIndexSetWrapper {
        private let mutableIndexSet: NSMutableIndexSet

        init(indexSet: NSMutableIndexSet) {
            self.mutableIndexSet = indexSet
        }

        init() {
            self.mutableIndexSet = NSMutableIndexSet()
        }
    }

    private let times = MutableIndexSetWrapper()

    mutating func addTimeRange(openTime: Int, closeTime: Int) {
        // Make sure our index set is only referenced by this struct instance
        if !isUniquelyReferencedNonObjC(&self.times) {
            self.times = MutableIndexSetWrapper(indexSet: NSMutableIndexSet(indexSet: self.times.mutableIndexSet))
        }

        let range = NSRange(location: openTime, length: closeTime - openTime)

        self.times.mutableIndexSet.addIndexesInRange(range)
    }
}