How to loop over struct properties in Swift?
Asked Answered
B

9

50

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?

Backing answered 4/12, 2014 at 10:57 Comment(3)
Have you been able to do this using the Sequence and IteratorProtocol?Personification
I never tried. It would be awesome if you could try it out and post an answer here :)Backing
Ha ha ha! I posted a question here. It seems using IteratorProtocol is relying on too much technology just for the sake of using technology because it's cool. I still want to do it that way. I got a couple of good questions. I went on to another task for my project, but I still have to come back to this. "matt"'s answer to my question seems to be the most practical. It uses KeyPath.Personification
D
45

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 ;)

Discomfit answered 16/11, 2015 at 9:51 Comment(8)
You could replace that loop with let properties = mirror.children.filter { $0.label != nil }.map { $0.label! }Mithridate
In Swift 2 you can make it even shorter with flatMap: let properties = mirror.children.flatMap { $0.label }Backing
But you it does not print static properties. Is there a way to achieve that?Backing
I have the same question @blackjacx, I think I will make another post.Michaelmichaela
@Backing #39339750Michaelmichaela
doesn't seem to work with static let struct properties.Pretermit
What about changing the value of a property?Punak
Could this be used to copy structs while changing a single property?Drillstock
J
25

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]
Jerrome answered 6/3, 2017 at 4:11 Comment(1)
Works perfectly, but it is possible to iterate over static properties?Pereyra
P
15

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 { ... }
Paske answered 3/9, 2019 at 17:33 Comment(2)
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 '_'Heideheidegger
Yes, this is just simplified, you should either do the check: if let dic = a.dictionary {...} or force unwrap (not recommeded)Paske
C
7

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)

Carlyn answered 19/6, 2019 at 19:58 Comment(0)
A
4
struct IdentifiersModel : Codable {
    let homeReuseID: String = "Home_Reuse_ID"
    let offersReuseID: String =  "Offers_Reuse_ID"
    let testReuseID: String =  "Test_Reuse_ID"

    func toDic() -> [String:Any] {
        var dict = [String:Any]()
        let otherSelf = Mirror(reflecting: self)
        for child in otherSelf.children {
            if let key = child.label {
                dict[key] = child.value
            }
        }
        return dict
    }
}

Create a new instance from the struct and call toDic() function

 let identifiersModel = IdentifiersModel()
  print(identifiersModel.toDic())

Output:

["offersReuseID": "Offers_Reuse_ID", "testReuseID": "Test_Reuse_ID", "homeReuseID": "Home_Reuse_ID"]
Aneto answered 20/10, 2021 at 11:26 Comment(0)
G
4

I found that RPatel99's answer worked the best for me, but it did not handle the case where there as an array of Loopables inside another Loopable.

Here is a version that fixes that issue.


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 (property, value) in mirror.children {
            var val = value

            if let values = value as? [Loopable] {
                var vals = [Any]()
                for val in values {
                    vals.append(val.allProperties())
                }
                val = vals
            }
            
            guard let prop = property 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
    }
}

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 = B()
    var f = [B(), B(), B()]
    var g = [1,2,3,4]
    var h: [String: Any] = ["A": 0, "B": B().allProperties()]
    var i: Int?
}
print(A().allProperties())





The result I get is this ["h": ["A": 0, "B": ["z": ["w": 14], "y": "BHello", "x": 12]], "e": ["y": "BHello", "z": ["w": 14], "x": 12], "f": [["y": "BHello", "z": ["w": 14], "x": 12], ["x": 12, "z": ["w": 14], "y": "BHello"], ["y": "BHello", "x": 12, "z": ["w": 14]]], "i": nil, "g": [1, 2, 3, 4], "a": 1, "d": "AHello", "c": 10.0]

I hope someone else will find this useful. This result could probably also be achieved by something like this

Germinant answered 10/8, 2022 at 22:7 Comment(0)
B
3

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 👎")
    }
}
Backing answered 4/12, 2014 at 10:57 Comment(3)
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.Discomfit
As for the example, the entire code can be removed using a storyboard, which is a much better solution.Hoxie
frankly this is better practice than relying on Reflection for implementationReyreyes
I
3

Knowing that in Swift 1.2 you could use reflect(), and since Swift 2 you can use Mirror, here is an addition to hris.to's answer for Swift 3 and 4.

protocol Loopable {
    var allProperties: [String: Any] { get }
}
extension Loopable {
    var allProperties: [String: Any] {
        var result = [String: Any]()
        Mirror(reflecting: self).children.forEach { child in
            if let property = child.label {
                result[property] = child.value
            }
        }
        return result
    }
}

Usage on any struct or class:

extension NSString: Loopable {}

print("hello".allProperties)
// ["_core": Swift._StringCore(_baseAddress: Optional(0x00000001157ee000), _countAndFlags: 5, _owner: nil)]
Inapproachable answered 13/3, 2017 at 16:27 Comment(0)
C
1

I built on top of Donovan Smith's excellent answer, I found with a struct that had a bunch of Optional and Wrapped values I ended up with values that were dictionaries that looked like ["some": "Value of optional"] when I really just wanted "Value of optional" as a String (or Int or whatever it was). This unpacks those values when they are discovered, or passes them unchanged when not.

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 (property, value) in mirror.children {
            var val = value
            
            if let values = value as? [Loopable] {
                var vals = [Any]()
                for val in values {
                    vals.append(val.allProperties())
                }
                val = vals
            }
            
            guard let prop = property else { continue }
            if limit == count {
                result[prop] = unpackValue(val)
            } else {
                let subResult = props(obj: val, count: count + 1, limit: limit)
                result[prop] = subResult.isEmpty ? unpackValue(val) : unpackValue(subResult)
            }
        }
        
        return result
    }
    
    private func unpackValue(_ value: Any) -> Any {
        var unpacked: Any
        var changed = false
        if let value = value as? [String: Any] {
            if let v1 = value["value"] {
                unpacked = v1
                changed = true
            } else if let v2 = value["some"] {
                unpacked = v2
                changed = true
            } else {
                unpacked = value
            }
        } else {
            unpacked = value
        }
        
        if let deeper = unpacked as? [String: Any], changed {
            return unpackValue(deeper)
        } else {
            return unpacked
        }
    }
}
Carolus answered 15/4 at 20:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.