How to loop over struct properties in Swift?

21,862

Solution 1

Although old question, with Swift evolving this question has new answer. I think that you approach is way better for the described situation, however original question was how to iterate over struct properties, so here is my answer(works both for classes and structs)

You can use Mirror Structure Reference. The point is that after calling reflect to some object you get it's "mirror" which is pretty sparingly however still useful reflection.

So we could easily declare following protocol, where key is the name of the property and value is the actual value:

protocol PropertyLoopable
{
    func allProperties() throws -> [String: Any]
}

Of course we should make use of new protocol extensions to provide default implementation for this protocol:

extension PropertyLoopable
{
    func allProperties() throws -> [String: Any] {

        var result: [String: Any] = [:]

        let mirror = Mirror(reflecting: self)

        guard let style = mirror.displayStyle where style == .Struct || style == .Class else {
            //throw some error
            throw NSError(domain: "hris.to", code: 777, userInfo: nil)
        }

        for (labelMaybe, valueMaybe) in mirror.children {
            guard let label = labelMaybe else {
                continue
            }

            result[label] = valueMaybe
        }

        return result
    }
}

So now we can loop over the properties of any class or struct with this method. We just have to mark the class as PropertyLoopable.

In order to keep things static(as in the example) I will add also a singleton:

struct ReuseID: PropertyLoopable {
    static let instance: ReuseID = ReuseID()
}

Whether singleton used or not, we could finally loop over the properties like follows:

do {
    print(try ReuseID.instance.allProperties())
} catch _ {

}

And that's it with looping struct properties. Enjoy swift ;)

Solution 2

Using hris.to's awesome answer, I wanted to provide a Swift 3 answer that's more to the point and doesn't use singletons.

Protocol & Extension:

protocol Loopable {
    func allProperties() throws -> [String: Any]
}

extension Loopable {
    func allProperties() throws -> [String: Any] {

        var result: [String: Any] = [:]

        let mirror = Mirror(reflecting: self)

        // Optional check to make sure we're iterating over a struct or class
        guard let style = mirror.displayStyle, style == .struct || style == .class else {
            throw NSError()
        }

        for (property, value) in mirror.children {
            guard let property = property else {
                continue
            }

            result[property] = value
        }

        return result
    }
}

Example:

struct Person: Loopable {
    var name: String
    var age: Int
}

var bob = Person(name: "bob", age: 20)

print(try bob.allProperties())

// prints: ["name": "bob", "age": 20]

Solution 3

Now there's a much easier way to do this:

1: Create an Encodable protocol extension:

extension Encodable {
    var dictionary: [String: Any]? {
        guard let data = try? JSONEncoder().encode(self) else { return nil }
        return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
    }
}

2: Make your struct/class conform to Encodable protocol

struct MyStruct: Encodable {...}
class MyClass: Encodable {...}

And then you can get a Dictionary representing your struct/class instance at any time:

var a: MyStruct
var b: MyClass

print(a.dictionary)
print(b.dictionary)

And then you can loop through the keys:

for (key, value) in a.dictionary { ... }
for (key, value) in b.dictionary { ... }

Solution 4

I made a recursive function based on @John R Perry's solution that goes deeper into properties that are objects. It also takes an parameter to limit how many levels deep it goes (default is Int.max) to help prevent stackoverflow's:

protocol Loopable {
    func allProperties(limit: Int) [String: Any]
}

extension Loopable {
    func allProperties(limit: Int = Int.max) [String: Any] {
        return props(obj: self, count: 0, limit: limit)
    }

    private func props(obj: Any, count: Int, limit: Int) -> [String: Any] {
        let mirror = Mirror(reflecting: obj)
        var result: [String: Any] = [:]
        for (prop, val) in mirror.children {
            guard let prop = prop else { continue }
            if limit == count {
                result[prop] = val
            } else {
                let subResult = props(obj: val, count: count + 1, limit: limit)
                result[prop] = subResult.count == 0 ? val : subResult
            }
        }
        return result
    }
}

I got rid of the check for if the object is a class or struct because that the parameter not being a class or struct is the base case of the recursive function, and it was easier to handle it manually than with errors.

Testing it:

class C {
    var w = 14
}
class B: Loopable {
    var x = 12
    var y = "BHello"
    var z = C()
    static func test() -> String {
        return "Test"
    }
}
class A: Loopable {
    var a = 1
    var c = 10.0
    var d = "AHello"
    var e = true
    var f = B()
    var g = [1,2,3,4]
    var h: [String: Any] = ["A": 0, "B": "Dictionary"]
    var i: Int?
}
print(A().allProperties())

prints:

["e": true, "g": [1, 2, 3, 4], "f": ["z": ["w": 14], "x": 12, "y": "BHello"], "h": ["A": 0, "B": "Dictionary"], "c": 10.0, "i": nil, "d": "AHello", "a": 1]

(Dictionaries are unordered, so if you get a different order, that's why)

Solution 5

Here is an example of iterating over struct properties (reuse identifiers of UITableViewCells and the corresponding NIB-names) using Swifts tuple feature. This is useful if you like organizing your cells in nib files and have a UIViewController that makes use of many different cell types.

struct ReuseID {
  static let prepaidRechargeCreditCell = "PrepaidRechargeCreditCell"
  static let threeTitledIconCell = "ThreeTitledIconCell"
  static let usageCell = "UsageCell"
  static let detailsCell = "DetailsCell"
  static let phoneNumberCell = "PhoneNumberCell"

  static let nibNamePrepaidRechargeCreditCell = "PrepaidRechargeCreditCell"
  static let nibNameThreeTitledIconCell = "IconCellWith3Titles"
  static let nibNameUsageCell = "ListElementRingViewCell"
  static let nibNameDetailsCell = "ListElementStandardViewCell"
  static let nibNamePhoneNumberCell = "PhoneNumberCell"

  static let allValuesAndNibNames = [
    (ReuseID.prepaidRechargeCreditCell, ReuseID.nibNamePrepaidRechargeCreditCell),          
    (ReuseID.threeTitledIconCell, ReuseID.nibNameThreeTitledIconCell), 
    (ReuseID.usageCell, ReuseID.nibNameUsageCell), 
    (ReuseID.detailsCell, ReuseID.nibNameDetailsCell), 
    (ReuseID.phoneNumberCell, ReuseID.nibNamePhoneNumberCell)]
}

With that struct it is easy to register all cell types using a for-loop:

for (reuseID, nibName) in ReuseID.allValuesAndNibNames {
    if let xibPath = NSBundle.mainBundle().pathForResource(nibName, ofType: "nib") {
        let fileName = xibPath.lastPathComponent.stringByDeletingPathExtension
        self.tableView.registerNib(UINib(nibName: fileName, bundle: nil), forCellReuseIdentifier: reuseID)

    } else {
        assertionFailure("Didn't find prepaidRechargeCreditCell 👎")
    }
}
Share:
21,862
blackjacx
Author by

blackjacx

I am a passionated iOS developer since 2009. To see the opn source repos I maintain please have a look at this gist

Updated on October 21, 2021

Comments

  • blackjacx
    blackjacx over 2 years

    Is it possible to iterate over properties of a struct in Swift?

    I need to register cells-reuse identifiers in a view controller that makes use of many different cell types (cells are organized in different nib files). So my idea was to put all reuse identifiers and the corresponding nib-files as static tuple-properties (reuseID, nibName) in a struct. But how can I iterate over all of them to register the cells with the tableView?

    I already tried something (see my answer below). But is there a more easy way to do this, e.g. without putting every property inside an array?

  • hris.to
    hris.to over 8 years
    That is not iterating over struct properties. If it was, adding new property should automatically return it by allValuesAndNibNames. I think the answer right now is NO.
  • Sulthan
    Sulthan over 8 years
    As for the example, the entire code can be removed using a storyboard, which is a much better solution.
  • Aaron Brager
    Aaron Brager over 8 years
    You could replace that loop with let properties = mirror.children.filter { $0.label != nil }.map { $0.label! }
  • blackjacx
    blackjacx about 8 years
    In Swift 2 you can make it even shorter with flatMap: let properties = mirror.children.flatMap { $0.label }
  • blackjacx
    blackjacx about 8 years
    But you it does not print static properties. Is there a way to achieve that?
  • yano
    yano almost 8 years
    frankly this is better practice than relying on Reflection for implementation
  • mattgabor
    mattgabor over 7 years
    I have the same question @blackjacx, I think I will make another post.
  • mattgabor
    mattgabor over 7 years
  • johndpope
    johndpope over 7 years
    doesn't seem to work with static let struct properties.
  • Julio Bailon
    Julio Bailon over 6 years
    What about changing the value of a property?
  • Petrus Theron
    Petrus Theron over 5 years
    Could this be used to copy structs while changing a single property?
  • AlbertUI
    AlbertUI over 3 years
    Works perfectly, but it is possible to iterate over static properties?
  • Benjamin B.
    Benjamin B. about 3 years
    got two errors trying this: For-in loop requires '[String : Any]?' to conform to 'Sequence'; did you mean to unwrap optional?, and Tuple pattern cannot match values of non-tuple type '_'
  • Frane Poljak
    Frane Poljak about 3 years
    Yes, this is just simplified, you should either do the check: if let dic = a.dictionary {...} or force unwrap (not recommeded)