CommaToast
CommaToast

Reputation: 12188

Iterating over and modifying a collection of structs in Swift

Suppose you have some structs like:

struct Tattoo {
    var imageTorso:UIImage?
    var imageTorsoURL:URL?
    var imageArms:UIImage?
    var imageArmsURL:URL?
}

struct Player {
    var name:String = ""
    var tattoos:[Tattoo] = []
}

struct Team {
    var name:String = ""
    var players:[Player] = []
}

Now imagine that you have a method that was passed in a Team value with some players. You have to iterate thru the players and their tattoos, then download the images and add them into the images variables on the tattoos.

If you use a for in loop, then it won't work because each part of the loop gets a copy of the members of the array it's iterating over. I.e.:

for player in team.players {
    for tattoo in player.tattoos {
        if let url = tattoo.imageTorsoURL {
            MyNetFramework.requestImage(from: url, completion: { image in
                tattoo.imageTorso = image
            }
        }
    }
}

After doing all the iterations and completion blocks, still, the original team variable is identical to what it was prior to doing any of this. Because each tattoo that the inner loop got was a copy of what is in the player's tattoos array.

Now I know you can use & to pass structs by reference in Swift but it's highly discouraged. As well I know you can use inout so they don't get copied when they come into functions, which is also discouraged.

I also know these could be made classes to avoid this behavior.

But supposing I don't have a choice in the matter -- they are structs -- it seems the only way to do this is something like:

for p in 0...team.players.count-1 {
    for t in 0...team.players[p].tattoos.count-1 {
        if let url = team.players[p].tattoos[t].imageTorsoURL {
            MyNetFramework.requestImage(from: url, completion: { image in
                team.players[p].tattoos[t].imageTorso = image
            }
        }
    }
}

This feels ugly and awkward, but I don't know how else to get around this thing where for in loops give you a copy of the thing you're iterating through.

Can anyone enlighten me, or is this just how it is in Swift?

Upvotes: 0

Views: 695

Answers (2)

Calvin Huang
Calvin Huang

Reputation: 95

I think you already got the point: "When your requirement will be modifying the data, you better to use class instead." Here is the question reference link for you. Why choose struct over class

struct is fast and you can use them to prevent creating a huge, messy class. struct provided the immutable feature and make us easier to follow the Function Programming

The most significant benefit of immutable data is free of race-condition and deadlocks. That because you only read the data and no worries about the problems caused by changing data.

However, to answer your question, I have few ways to solve it.

1. Renew whole data.

// First, we need to add constructors for creating instances more easier.
struct Tattoo {
    var imageTorso:UIImage?
    var imageTorsoURL:URL?
    var imageArms:UIImage?
    var imageArmsURL:URL?

    init(imageTorso: UIImage? = nil, imageTorsoURL: URL? = nil, imageArms: UIImage? = nil, imageArmsURL: URL? = nil) {
        self.imageTorso = imageTorso
        self.imageTorsoURL = imageTorsoURL
        self.imageArms = imageArms
        self.imageArmsURL = imageArmsURL
    }
}

struct Player {
    var name:String
    var tattoos:[Tattoo]

    init() {
        self.init(name: "", tattoos: [])
    }

    init(name: String, tattoos: [Tattoo]) {
        self.name = name
        self.tattoos = tattoos
    }
}

struct Team {
    var name:String
    var players:[Player]

    init() {
        self.init(name: "", players: [])
    }

    init(name: String, players: [Player]) {
        self.name = name
        self.players = players
    }
}

for player in team.players {
    for tattoo in player.tattoos {
        if let url = tattoo.imageTorsoURL {

            // Catch old UIImage for matching which Tattoo need to be updated.
            ({ (needChangeImage: UIImage?) -> Void in
                MyNetFramework.requestImage(from: url, completion: { image in

                    // Reconstruct whole team data structure.
                    let newPlayers = team.players.map { (player) -> Player in
                        let newTattos = player.tattoos.map { (tattoo) -> Tattoo in
                            if needChangeImage == tattoo.imageTorso {
                                return Tattoo(imageTorso: image)
                            } else {
                                return tattoo
                            }
                        }
                        return Player(name: player.name, tattoos: newTattos)
                    }
                    team = Team(name: team.name, players: newPlayers)
                })
            })(tattoo.imageTorso)
        }
    }
}

These codes are ugly, right? And there will not only be awful performance issue caused by going through whole data every network response; another problem is that might causes the retain cycle.

2. Don't hold UIImage in the data array.

Redesign your data structure, and use Kingfisher to help us download image synchronously.

Kingfisher is useful third party library. It provides clean and simple methods to use, and it's highly flexible.

let url = URL(string: "url_of_your_image")
imageView.kf.setImage(with: url)

However, I think the best way for you if you don't want to use Kingfisher is to change your declaration from struct to class.

Upvotes: 2

Code Different
Code Different

Reputation: 93161

Unfortunately that's the nature of struct and Swift doesn't offer a way for you modify the collection in-place while iterating over it. But can user enumerated() to get both the index and the element when iterating:

for (p, player) in team.players.enumerated() {
    for (t, tattoo) in player.tattoos.enumerated() {
        if let url = tattoo.imageTorsoURL {
            MyNetFramework.requestImage(from: url, completion: { image in
                team.players[p].tattoos[t].imageTorso = image
            }
        }
    }
}

Upvotes: 1

Related Questions