Search code examples
swiftswiftuiobservedobject

Swift Observe dynamic dictionary


I'm trying to listen to changes of an element in a dictionary using @Published.

I have a dictionary, containing a list of player scores, indexed by player id. It looks like this:

class PlayersScore: ObservableObject {
    @Published var scores: [String: PlayerScore] = [:];
}

This dictionary is exposed through the property scores of a shared instance of a class called Api.

class Api: NSObject {
   static var shared = Api()
   @Published public var scores = PlayersScore()

   func updatePlayerScore(playerId: String, score: PlayerScore) {
       this.scores.scores[playerId] = score
   }
}

Sometimes, the score of a player is updated.

I have a view dedicated to a Player that looks like this:

struct PlayerView: View {
   var player: Player
   @ObservedObject private var scores = Api.shared.scores

   var body: some View {
       HStack {
           Text(player.name)
           Text("Latest score: \(scores.scores[player.id] ?? "not found")")
       }
   }
}

When the score of a player is updated, my view re-render.
My problem is that ... when a player score is updated, ALL players views are re-rendered as they all react to changes of the main dictionary.

So, I would like to subscribe to changes on a specific player score in his corresponding view, so only the appropriate view re-renders when necessary.

The player score may not be yet initialised when subscribing or accessing (ie scores.scores[player.id] may not exists yet).

Any idea how I could achieve this?

Thanks!


Solution

  • Here is a possible approach - make PlayerScore as a observable class, so PlayersScore will be updated only when new player added, and due to observed specific PlayerScore explicitly only corresponding view will be updated on value changes.

    class PlayerScore: ObservableObject {
      var id: String  // say back ref to user
    
      @Published var value: Int
      // ... other published properties
    }
    
    struct PlayerView: View {
       var player: Player
       @ObservedObject private var scores = Api.shared.scores
    
       var body: some View {
           HStack {
               Text(player.name)
               if let score = scores.scores[player.id] {
                  PlayerScoreView(playerScore: score)
               } else {
                  Text("No scores")
               }
           }
       }
    }
    
    struct PlayerScoreView: View {
      @ObservedObject var playerScore: PlayerScore
    
      var body: some View {
         Text("Latest score: \(playerScore.value)")
      }
    }
    

    Note: this is just a sketch coded here, so might be typo