Noodle of Death Noodle of Death - 3 months ago 12
Swift Question

Define struct that is treated like a class in Swift

In Swift a

String
structure is also treated as a class object like when using the
NSCoder
encodeObject(_:forKey:)
method. I do know that
String
is directly bridged with the objective-c class,
NSString
, but is there a way to make a custom
struct
that behaves similarly? Perhaps bridge it to a custom class? I want to be able to do something like this:

struct SortedArray <Value: Comparable> {}

// Would I need to create a bridge between
// SortedArray and NSSortedArray? Can I even do that?
class NSSortedArray <Value: Comparable> : NSObject, NSCoding {
required init?(coder aDecoder: NSCoder) {}
func encodeWithCoder(aCoder: NSCoder) {}
}

class MyClass : NSObject, NSCoding {
private var objects: SortedArray<String> = SortedArray<String>()
required init?(coder aDecoder: NSCoder) {
guard let objects = aDecoder.decodeObjectForKey("objects") as? SortedArray<String> else { return nil }
self.objects = objects
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(objects, forKey: "objects")
}
}

Answer

I found a working, elegant solution that works with an _ObjectiveCBridgeable struct that can be encoded by NSCoder; thanks to the reference that Rob Napier provided. Here is the sample code for anyone interested (I still need to finish documenting it). Using the library code I wrote below, I can now do something like this:

func init?(coder aDecoder: NSCoder) {
    guard let numbers = aDecoder.decodeObjectForKey("Numbers") as? SortedArray<Int> else { return nil }
    print(numbers) // Outputs "[1,3,5]"
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(SortedArray<Int>([1,5,3]), forKey: "Numbers")
}

SortedArray.swift

//
//  SortedArray.swift
//  NoodleKit
//
//  Created by Thom Morgan on 8/15/16.
//  Copyright © 2016 CocoaPods. All rights reserved.
//

import Foundation

public enum SortOrder : Int {
    case Ascending, Descending
}

/// An array data structure that automatically places elements in order as
/// they added to the collection.
public struct SortedArray <Value: Comparable> : CollectionType, _ObjectiveCBridgeable, CustomStringConvertible {

    public typealias _ObjectiveCType = NSSortedArray<Value>

    public typealias Index = Int
    public typealias Generator = IndexingGenerator<SortedArray<Value>>

    public var startIndex: Index {
        return 0
    }

    public var endIndex: Index {
        return values.count
    }

    public var range: Range<Index> {
        return 0 ..< values.count
    }

    public var count: Int {
        return values.count
    }

    public var uniqueElements: Bool = true

    public var description: String {
        return "\(values)"
    }

    public let sortOrder: SortOrder
    public private (set) var values = [Value]()

    // MARK: - ** Constructor Methods **

    public init(sortOrder: SortOrder = .Ascending, values: [Value] = [Value]()) {
        self.sortOrder = sortOrder
        self.values = values.sort({ (a: Value, b: Value) -> Bool in
            return sortOrder == .Ascending ? (a < b) : (b < a)
        })
    }

    public init(_ values: [Value]) {
        sortOrder = .Ascending
        self.values = values.sort({ (a: Value, b: Value) -> Bool in
            return a < b
        })
    }

    public init(_ sortedArray: NSSortedArray<Value>) {
        sortOrder = sortedArray.sortOrder
        values = sortedArray.values
    }

    // MARK: - ** Protocol Methods (_ObjectiveCBridgeable) **

    public static func _isBridgedToObjectiveC() -> Bool {
        return true
    }

    public static func _getObjectiveCType() -> Any.Type {
        return _ObjectiveCType.self
    }

    public static func _forceBridgeFromObjectiveC(source: _ObjectiveCType, inout result: SortedArray<Value>?) {
        result = SortedArray<Value>(source)
    }

    public static func _conditionallyBridgeFromObjectiveC(source: _ObjectiveCType, inout result: SortedArray<Value>?) -> Bool {
        _forceBridgeFromObjectiveC(source, result: &result)
        return true
    }

    public func _bridgeToObjectiveC() -> _ObjectiveCType {
        return NSSortedArray(self)
    }

    // MARK: - ** Subscript Methods **

    public subscript (index: Index) -> Value {
        get { return values[index] }
        set { values[index] = newValue }
    }

    // MARK: - ** Public Methods **

    public func generate() -> Generator {
        return Generator(SortedArray(values: values))
    }

    /// - complexity: O(`log(self.count)`)
    public func indexOf(value: Value, searchRange range: Range<Index>? = nil) -> Index? {

        if values.count == 0 { return nil }

        let range = range ?? 0 ..< values.count
        let index = (range.startIndex + range.length / 2)
        let val = values[index]

        if range.length == 1 {
            return val == value ? index : nil
        } else if (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) {
            return indexOf(value, searchRange: range.startIndex ..< index)
        }

        return indexOf(value, searchRange: index ..< range.endIndex)

    }

    /// - complexity: O(`log(self.count)`)
    public func ordinalIndexForValue(value: Value, searchRange range: Range<Index>? = nil) -> Index {

        if values.count == 0 { return 0 }

        let range = range ?? 0 ..< values.count
        let index = (range.startIndex + range.length / 2)
        let val = values[index]

        if range.length == 1 {
            return (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) ? index : index + 1
        } else if (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) {
            return ordinalIndexForValue(value, searchRange: range.startIndex ..< index)
        }

        return ordinalIndexForValue(value, searchRange: index ..< range.endIndex)

    }

    public mutating func add(value: Value) -> Index? {
        var index = 0
        if values.count == 0 { values.append(value) }
        else {
            if uniqueElements && indexOf(value) != nil { return nil }
            index = ordinalIndexForValue(value)
            values.insert(value, atIndex: index)
        }
        return index
    }

    public mutating func remove(value: Value){
        var index = indexOf(value)
        while index != nil {
            values.removeAtIndex(index!)
            index = indexOf(value)
        }
    }

    public mutating func insert(value: Value, atIndex index: Index) {
        values.insert(value, atIndex: index)
    }

    public mutating func removeAtIndex(index: Index) -> Value {
        return values.removeAtIndex(index)
    }

    public mutating func removeAll() {
        values.removeAll()
    }

}

NSSortedArray.swift

//
//  NSSortedArray.swift
//  NoodleKit
//
//  Created by Thom Morgan on 6/29/16.
//  Copyright © 2016 NoodleOfDeath. All rights reserved.
//

import Foundation

private struct CodingKeys {
    static let SortOrder = "SortOrder"
    static let Values = "Values"
}

public class NSSortedArray <Value: Comparable> : NSObject, CollectionType, NSCoding {

    public typealias Index = Int
    public typealias Generator = IndexingGenerator<NSSortedArray<Value>>

    public var startIndex: Index {
        return 0
    }

    public var endIndex: Index {
        return values.count
    }

    public var range: Range<Index> {
        return 0 ..< values.count
    }

    public var count: Int {
        return values.count
    }

    public var uniqueElements: Bool = true

    public override var description: String {
        return "\(values)"
    }

    public let sortOrder: SortOrder
    public private (set) var values = [Value]()

    // MARK: - ** Constructor Methods **

    public init(sortOrder: SortOrder = .Ascending, values: [Value] = [Value]()) {
        self.sortOrder = sortOrder
        self.values = values.sort({ (a: Value, b: Value) -> Bool in
            return sortOrder == .Ascending ? (a < b) : (b < a)
        })
    }

    public init(_ values: [Value]) {
        sortOrder = .Ascending
        self.values = values.sort({ (a: Value, b: Value) -> Bool in
            return a < b
        })
    }

    public init(_ sortedArray: SortedArray<Value>) {
        sortOrder = sortedArray.sortOrder
        values = sortedArray.values
    }

    public convenience required init?(coder aDecoder: NSCoder) {
        guard let sortOrder = SortOrder(rawValue: aDecoder.decodeIntegerForKey(CodingKeys.SortOrder)) else { return nil }
        guard let values = aDecoder.decodeObjectForKey(CodingKeys.Values) as? [Value] else { return nil }
        self.init(sortOrder: sortOrder, values: values)
    }

    public func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeInteger(sortOrder.rawValue, forKey: CodingKeys.SortOrder)
        guard let values = values as? NSCoding else { return }
        aCoder.encodeObject(values, forKey: CodingKeys.Values)
    }

    // MARK: - ** Subscript Methods **

    public subscript (index: Index) -> Value {
        get { return values[index] }
        set { values[index] = newValue }
    }

    public func generate() -> Generator {
        return Generator(NSSortedArray(values: values))
    }

    /// - complexity: O(`log(self.count)`)
    public func indexOf(value: Value, searchRange range: Range<Index>? = nil) -> Index? {

        if values.count == 0 { return nil }

        let range = range ?? 0 ..< values.count
        let index = (range.startIndex + range.length / 2)
        let val = values[index]

        if range.length == 1 {
            return val == value ? index : nil
        } else if (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) {
            return indexOf(value, searchRange: range.startIndex ..< index)
        }

        return indexOf(value, searchRange: index ..< range.endIndex)

    }

    /// - complexity: O(`log(self.count)`)
    public func ordinalIndexForValue(value: Value, searchRange range: Range<Index>? = nil) -> Index {

        if values.count == 0 { return 0 }

        let range = range ?? 0 ..< values.count
        let index = (range.startIndex + range.length / 2)
        let val = values[index]

        if range.length == 1 {
            return (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) ? index : index + 1
        } else if (val > value && sortOrder == .Ascending) || (val < value && sortOrder == .Descending) {
            return ordinalIndexForValue(value, searchRange: range.startIndex ..< index)
        }

        return ordinalIndexForValue(value, searchRange: index ..< range.endIndex)

    }

    public func add(value: Value) -> Index? {
        var index = 0
        if values.count == 0 { values.append(value) }
        else {
            if uniqueElements && indexOf(value) != nil { return nil }
            index = ordinalIndexForValue(value)
            values.insert(value, atIndex: index)
        }
        return index
    }

    public func remove(value: Value){
        var index = indexOf(value)
        while index != nil {
            values.removeAtIndex(index!)
            index = indexOf(value)
        }
    }

    public func insert(value: Value, atIndex index: Index) {
        values.insert(value, atIndex: index)
    }

    public func removeAtIndex(index: Index) -> Value {
        return values.removeAtIndex(index)
    }

    public func removeAll() {
        values.removeAll()
    }

}

NSCoder.swift

//
//  NSCoder.swift
//  NoodleKit
//
//  Created by Thom Morgan on 8/15/16.
//  Copyright © 2016 CocoaPods. All rights reserved.
//

// MARK: - ** Import Modules **

import Foundation

// MARK: - ** NSCoder _ObjectiveCBridgeable Encoding Compatabiltiy **

extension NSCoder {

    /// Encodes an `_ObjectiveCBridgeable` data structure.
    /// - important: The objective-c class being bridged to must conform to
    /// `NSCoding`.
    /// - parameter object: The object to encode.
    public func encodeObject<T: _ObjectiveCBridgeable>(object: T?) {
        encodeObject(object?._bridgeToObjectiveC())
    }

    /// Encodes an `_ObjectiveCBridgeable` data structure as a root object.
    /// - important: The objective-c class being bridged to must conform to
    /// `NSCoding`.
    /// - parameter object: The object to encode.
    public func encodeRootObject<T: _ObjectiveCBridgeable>(object: T) {
        encodeRootObject(object._bridgeToObjectiveC())
    }

    /// Encodes an `_ObjectiveCBridgeable` conditional data structure.
    /// - important: The objective-c class being bridged to must conform to
    /// `NSCoding`.
    /// - parameter object: The object to encode.
    public func encodeConditionalObject<T: _ObjectiveCBridgeable>(object: T?) {
        encodeConditionalObject(object?._bridgeToObjectiveC())
    }

    /// Encodes an `_ObjectiveCBridgeable` data structure and maps it to a 
    /// specific `key`.
    /// - important: The objective-c class being bridged to must conform to
    /// `NSCoding`.
    /// - parameter object: The object to encode.
    /// - parameter key: The key to associate with this object.
    public func encodeObject<T: _ObjectiveCBridgeable>(object: T?, forKey key: String) {
        encodeObject(object?._bridgeToObjectiveC(), forKey: key)
    }

    /// Encodes an `_ObjectiveCBridgeable` conditional data structure and maps
    /// it to a specific `key`.
    /// - important: The objective-c class being bridged to must conform to
    /// `NSCoding`.
    /// - parameter object: The object to encode.
    /// - parameter key: The key to associate with this object.
    public func encodeConditionalObject<T: _ObjectiveCBridgeable>(object: T?, forKey key: String) {
        encodeConditionalObject(object?._bridgeToObjectiveC(), forKey: key)
    }

}
Comments