How to get all property names in nested Structs - swift

Lets assume I have the following struct:
struct Location: Codable, Loopable {
var altitude: Double?
var coordinate: Coordinate?
struct Coordinate: Codable, Loopable {
var latitude: Double?
var longitude: Double?
}
var course: Double?
var courseAccuracy: Double?
var floor: Floor?
struct Floor: Codable, Loopable {
var level: Int?
}
var horizontalAccuracy: Double?
var speed: Double?
var speedAccuracy: Double?
var timestamp: Timestamp?
struct Timestamp: Codable, Loopable {
var year: Int?
var month: Int?
var day: Int?
var hour: Int?
var minute: Int?
var second: Int?
}
var verticalAccuracy: Double?
var deviceName: String?
var validPosition: Bool?
}
Now I want to implement two methods for structs. One should return all property names with all parents in its name and the other one should return all values of these properties.
The result should look like this for the first method (lets name it allProperties()):
["altitude", "coordinate.latitude", "coordinate.longitude", "course", "courseAccuracy", "floor.level", "horzontalAccuracy", "speed", "speedAccuracy", "timeststamp.year", "timestamp.month", "timestamp.day", "timeststamp.hour", "timestamp.minute", "timestamp.second", "verticalAccuracy", "deviceName", "validPosition"]
The result of the second method (lets name it allValues()) should look like this:
[500.0, 48.000000, 10.00000, 120.0, 5.0, 4, 5.0, 3.0, 1.0, 2021, 07, 25, 22, 43, 50, 10.0, "iPhone", true]
As you can see my example struct is nested with other structs. In my real project, the structs have more than two nested "levels". Also the property types are not unique. On the very bottom level they are all basic data types (Double, Bool, String, Int).
I tried to modify the solution for parsing nested structs with an recursive approach from this thread, which seems to be an elegant solution: How to loop over struct properties in Swift?
Now my problem:
The method for the property names only gives an array with "some" back.
But the method for the property values returns the correct Array
["some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some", "some"]
This was my code for the protocol so far:
protocol Loopable {
func allProperties(limit: Int) -> [String]
func allValues(limit: Int) -> [Any]
}
extension Loopable {
func allProperties(limit: Int = Int.max) -> [String] {
return props(obj: self, count: 0, limit: limit)
}
func allValues(limit: Int = Int.max) -> [Any] {
return values(obj: self, count: 0, limit: limit)
}
private func props(obj: Any, count: Int, limit: Int) -> [String] {
let mirror = Mirror(reflecting: obj)
var result: [String] = []
for (prop, val) in mirror.children {
guard let prop = prop else { continue }
if limit == count {
result.append(prop)
} else {
let subResult = props(obj: val, count: count + 1, limit: limit)
subResult.count == 0 ? result.append(prop) : result.append(contentsOf: subResult)
}
}
return result
}
private func values(obj: Any, count: Int, limit: Int) -> [Any] {
let mirror = Mirror(reflecting: obj)
var result: [Any] = []
for (_, val) in mirror.children {
//guard let val = val else { continue } // This line does not compile
if limit == count {
result.append(val)
} else {
let subResult = values(obj: val, count: count + 1, limit: limit)
subResult.count == 0 ? result.append(val) : result.append(contentsOf: subResult)
}
}
return result
}
}
What am I doing wrong here? Why are the property labels always "some"?

So the issue with the “some” stems from your values being optional, optional values are an enumeration so your reflection is picking that up. This is why you cannot perform the guard in private func values(obj: Any, count: Int, limit: Int) -> [Any] as you are not unwrapping to a concrete type.
As all your subtypes confirm to Loopable we can refactor your code to check if the type conforms to Loopable. It is also more efficient to check if something is empty than if its count is equal to zero, so you should use that property where you can.
We’re now using the prop value as a prefix so that we can get the name that you are looking for, however, because your values are optional they are wrapped in an enum so you have to strip the .some from the string. The type of val is Any, this means we cannot unwrap it if it is an optional without knowing what its concrete type is, hence we need to do the above dance to remove the .some from the prefix.
import Foundation
protocol Loopable {
func allProperties() -> [String]
}
extension Loopable {
func allProperties() -> [String] {
return props(obj: self)
}
private func props(obj: Any, prefix: String = "") -> [String] {
let mirror = Mirror(reflecting: obj)
var result: [String] = []
for (prop, val) in mirror.children {
guard var prop = prop else { continue }
// handle the prefix
if !prefix.isEmpty {
prop = prefix + prop
prop = prop.replacingOccurrences(of: ".some", with: "")
}
if let _ = val as? Loopable {
let subResult = props(obj: val, prefix: "\(prop).")
subResult.isEmpty ? result.append(prop) : result.append(contentsOf: subResult)
} else {
result.append(prop)
}
}
return result
}
}
Here is a simple struct that we can test the above code with.
struct User: Loopable {
let name: String
let age: Age?
struct Age: Loopable {
let value: Int?
let day: Day?
struct Day: Loopable {
let weekday: Int?
}
}
}
let user = User(name: "mike", age: .init(value: 20, day: .init(weekday: 5)))
print(user.allProperties())
This will print out the following
["name", "age.value", "age.day.weekday"]

Related

Can we use for loop for items of a struct?

I have a struct like this:
struct TestType {
var value1: String? = "1"
var value2: String? = "2"
var value3: String? = "3"
var value4: String? = "4"
var value5: String? = "5"
var value6: String? = "6"
var value7: String? = "7"
var value8: String? = "8"
var value9: String? = "9"
}
I want be able to use a for loop on values of TestType, like this code in below, is this possible in swift?
Or even any kind of loop support for items of a struct?
func myTestFunction() {
let test: TestType = TestType()
test(value1...value9).forEach { value in
if let unwrappedValue: String = value {
print(unwrappedValue)
}
}
}
You can use Mirror to achieve that, something like this:
struct TestType {
var value1: String? = "1"
var value2: String? = "2"
var value3: String? = "3"
var value4: String? = "4"
var value5: String? = "5"
var value6: String? = "6"
var value7: String? = "7"
var value8: String? = "8"
var value9: String? = "9"
func iterateThroughProperties() {
for property in Mirror(reflecting: self).children where property.label != nil {
print("name: \(property.label!)")
print("value: \(property.value)")
print("type: \(type(of: property.value))")
}
}
}
[String?](mirrorChildValuesOf: TestType())
public extension Array {
/// Create an `Array` if `subject's` values are all of one type.
/// - Note: Useful for converting tuples to `Array`s.
init?<Subject>(mirrorChildValuesOf subject: Subject) {
guard let array =
Mirror(reflecting: subject).children.map(\.value)
as? Self
else { return nil }
self = array
}
}

How can i disable adding new item to an array in Swift?

I have an array like this inside a struct:
struct TestType {
private(set) var array: [String] = ["0", "1", "2"]
mutating func updateItem0(_ value: String) {
self.array[0] = value
}
mutating func updateItem1(_ value: String) {
self.array[1] = value
}
mutating func updateItem2(_ value: String) {
self.array[2] = value
}
}
I want be able to disable appending method to this array when I use an instance of struct, and keeping the count of it as it is. I cannot use private(set) because it would not allow me to update items of it.
My idea is using private(set) inside struct and making a mutating function for updating items in case, I wanted to know if there is better way for it?
Lots of options but a simple enhancement would be passing the index:
mutating func update(_ value: String, at index: Int) {
array[index] = value
}
And another is to check if the operation is possible:
enum Error: Swift.Error {
case indexOutOfBound
}
mutating func update(_ value: String, at index: Int) throws {
guard array.indices.contains(index) else { throw Error.indexOutOfBound }
array[index] = value
}
Here is a nice way to handle it. Add subscript to your struct which then allows you to access and change the values like you would an array. Adopting CustomStringConvertible and implementing description allows you to print the internal array while keeping it entirely private:
struct TestType: CustomStringConvertible {
private var array: [String] = ["0", "1", "2"]
var description: String { String(describing: array) }
subscript(_ index: Int) -> String {
get {
return array[index]
}
set {
array[index] = newValue
}
}
}
var foo = TestType()
foo[0] = "hello"
foo[2] = "goodbye"
foo[3] = "oops" // Fatal error: Index out of range
print(foo[0]) // hello
print(foo[1]) // 1
print(foo[2]) // goodbye
print(foo) // ["hello", "1", "goodbye"]

How to create a protocol conform Comparable in Swift

I have a use case to compare the enployee's rank. Here is what I want to do:
protocol Enployee: Comparable {
var id: String { get }
var rank: Int { get }
var name: String { get }
var type: String { get }
}
extension Enployee {
static func <(lhs: Enployee, rhs: Enployee) -> Bool {
return lhs.rank < rhs.rank
}
}
But I got the following error:
Protocol 'Enployee' can only be used as a generic constraint because it has Self or associated type requirements
Then I changed my code:
extension Enployee {
static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.rank < rhs.rank
}
}
I can compile it. But when I continue working on my user case:
struct Engineer: Enployee {
var id: String
var rank: Int
var name: String
let type: String = "Engineer"
}
struct Manager: Enployee {
var id: String
var rank: Int
var name: String
let type: String = "Manager"
}
let staff1 = Engineer(id: "123", rank: 2, name: "Joe")
let staff2 = Engineer(id: "124", rank: 2, name: "Frank")
let staff3 = Manager(id: "101", rank: 10, name: "John")
public struct Department<T: Comparable> {
}
let queue = Department<Enployee>()
I got another error message:
Protocol 'Enployee' as a type cannot conform to 'Comparable'
Any idea?
The error message tells you what the problem is. Having declared Department<T: Comparable> you cannot resolve T as Enployee; it is a protocol, not a type conforming to Comparable (such as Engineer).
A generic has to be resolved to one type. It would be legal to declare Department<T: Enployee> if that is what you really mean. But then you are setting yourself up for multiple Department types, a department of Manager and a department of Engineer. That is probably not what you want, so this is a bad use of a generic.
Moreover, type strings are a horrifically bad smell:
struct Engineer: Enployee {
var id: String
var rank: Int
var name: String
let type: String = "Engineer" // uck
}
It seems much more likely that you want a class and subclasses here. Why fight against OOP?

Accept Float, Double or Int in Swift `init` to convert to String

I'm attempting to convert a value to a string in an init. That value can be an Int, Double, or Float.
For example:
struct example {
var string: String
init(number: Int) {
string = String(number)
}
}
I would like to say something like below (I'm using Typescript as an example - this obviously doesn't work, but I want number to be any of those three types - all of which String can convert).
struct example {
var string: String
init(number: Int | Float | Double) {
string = String(number)
}
}
Edit: I realized I have another issue. I need to convert back from a string to the type of Int or Double or Float. Using one of the answers below, I'm trying to figure out how to implement getNumberWith5:
struct example2<N: Numeric & CustomStringConvertible>: CustomStringConvertible {
#Binding var number: N
init(number: Binding<N>) {
self._number = number
}
var description: String {
String(describing: number)
}
mutating func getNumberWith5() {
// How do I update number to the type N?
self.number = howDoIConvertToN(description + "5")
}
}
Or from another answer:
struct example3<N: Numeric> {
#Binding var number: N
var string: String
init(number: Binding<N>) {
self._number = number
self.string = "\(number)"
}
mutating func getNumberWith5() {
// How do I update number to the type N?
self.number = howDoIConvertToN(string + "5")
}
}
Edit2 My Answer:
I attempted to create an equivalent of type unions (as Typescript has) using enums in Swift based on this article. But it was challenging to then assign back to that value. I've decided Swift just doesn't have first class support for type unions like Typescript has. So, I used the accepted answer below and this seems to work.
extension String {
func numeric<N: Numeric & LosslessStringConvertible>() -> N? {
N(self)
}
}
struct example4<N: Numeric & LosslessStringConvertible> {
#State var string: String
#Binding var number: N
init(number: Binding<N>) {
self._number = number
self.string = String(describing: number)
}
mutating func getNumberWith5() {
let newString = string + "5"
number = newString.numeric() ?? 0
}
}
Actually if all you want is a string representation of Int Float Double or any other standard numeric type you only need to know that they conform to CustomStringConvertible and use String(describing:).
Or you can use conformance to Numeric and CustomStringConvertible:
struct example {
var string: String
init<C: CustomStringConvertible & Numeric>(number: C) {
string = String(describing: number)
}
}
and maybe even better example itself could conform to CustomStringConvertible
struct example: CustomStringConvertible {
var description: String
init<C: CustomStringConvertible & Numeric>(number: C) {
description = String(describing: number)
}
}
yet another way :
struct example<N: Numeric & CustomStringConvertible>: CustomStringConvertible {
let number: N
init(number: N) {
self.number = number
}
var description: String {
String(describing: number)
}
}
EDIT
I think what you want is a custom Property Wrapper not #Binding:
#propertyWrapper struct CustomStringConversion<Wrapped: CustomStringConvertible> {
var wrappedValue: Wrapped
init(wrappedValue: Wrapped) {
self.wrappedValue = wrappedValue
}
var projectedValue: String { .init(describing: wrappedValue) }
}
struct Foo {
#CustomStringConversion var number = 5
}
let foo = Foo()
let number: Int = foo.number // 5
let stringRepresentation: String = foo.$number // "5"
But as #LeoDabus pointed out using LosslessStringConvertible may be better :
struct example<N: Numeric & LosslessStringConvertible>: LosslessStringConvertible {
let number: N
init(number: N) {
self.number = number
}
init?(_ description: String) {
guard let number = N(description) else { return nil }
self.number = number
}
var description: String {
.init(number)
}
}
let bar = example(number: Double.greatestFiniteMagnitude) // 1.7976931348623157e+308
let baz: example<Double>? = example("1.7976931348623157e+308") // 1.7976931348623157e+308
Use generic structure with Numeric protocol.
struct Example<T:Numeric> {
var string: String
init(number: T) {
self.string = "\(number)"
}
}
struct Example {
var string: String
init(number: Int) {
string = String(number)
}
init(number: Float) {
string = String(number)
}
init(number: Double) {
string = String(number)
}
}
You can have a look at how swift does this with its String-initializer:
struct Example {
init<Number>(number: Number) where Number: BinaryInteger {
string = String(number)
}
}

Is there a way to make this function generic?

I have a data set:
struct MyData {
var name: String
var date: Date
var quantity: Int
var inStock: Bool
}
That is then used in an array
let myArray: [MyData] = [...]
And I'm currently writing methods to change values..
func changeName(to newName: String) { myArray[0].name = newName }
func changeDate(to newDate: Date) { myArray[0].date = newDate }
func changeQuantity(to newQuantity: Int) { myArray[0].quantity = newQuantity}
And I start thinking that this would be a perfect case for generics and say something like
func changeValue<Value>(to newValue: Value) { ... }
only problem is I can't make the dot notation generic
If I could say
myArray[0].value = newValue
Any guidance on the matter would be helpful!
If you var declare your properties you can add the following function to your struct
mutating func setValue<Value>(_ value: Value, for keyPath: WritableKeyPath<Self, Value>) {
self[keyPath: keyPath] = value
}
And then use it like
var myData = MyData(name: "abs", date: Date(timeIntervalSince1970: 0), quantity: 0, inStock: false)
myData.setValue(500, for: \.quantity )
myData.setValue(Date(), for: \.date)
myData.setValue(true, for: \.inStock)
which will update the object
MyData(name: "abs", date: 2021-03-27 18:26:26 +0000, quantity: 500, inStock: true)
Then how you want to use it for your array is a bit unclear to me but here you at least have a tool to work with.