SwiftUI Observe Changes

Overview:

  • I have a class called
    Player
    and a class called
    Song
    .
  • Player
    contains a
    Song
  • A view is showing the song title

Aim:

When I change the

player.song.title
, the view needs to be updated.

Problem:

When the song's attributes changes, it wouldn't update the view automatically. Only when a new song is assigned will the changes be reflected.

My Attempts:

I have made 2 attempts (code below), both work as expected.

Questions:

  • Is there a better way to do it ? (It seems like a common problem, one would encounter.)
  • Are my attempts reasonable and is attempt 2 better?
  • Or is there something fundamentally flawed with my design? (I was hoping to have the song inside the player because it represented the current song).

Original Code (view wouldn't be updated):

Model

import Foundation
import Combine


class Player : ObservableObject {
    
    @Published var duration = 0
    @Published var song     : Song
    
    init(song: Song) {
        self.song = song
    }
}


class Song : ObservableObject {
    
    @Published var id     : Int
    @Published var title  : String
    @Published var artist : String
    
    init(id: Int,
         title: String,
         artist: String) {


        self.id     = id
        self.title  = title
        self.artist = artist
    }
}

View:

import SwiftUI


struct ContentView: View {
    
    @ObservedObject var player : Player
    
    var body: some View {
        
        VStack {
            Text(String(player.duration))
            Text(player.song.title)
            Text(player.song.artist)
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        
        let song = Song(id: 1, title: "title1", artist: "artist1")
        let player = Player(song: song)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            player.song.title = "title2"
        }
        
        return ContentView(player: player)
    }
}


Attempt1 - Using

CombineLatest

Problem: It is not very scalable as the number of properties in a song increases.

class Player : ObservableObject {
    
    @Published var duration = 0
    @Published var song     : Song
    private var songChangeCanceller : AnyCancellable?
    
    init(song: Song) {
        self.song = song
        
        songChangeCanceller = song.$title.combineLatest(song.$artist, song.$id).sink { _, _, _ in
            self.objectWillChange.send()
        }
    }
}


Attemp2: Uses

objectWillChange.sink

class Player : ObservableObject {
    
    @Published var duration = 0
    @Published var song     : Song


    private var songChangeCanceller : AnyCancellable?
    private var songAttributesChangeCanceller : AnyCancellable?


    init(song: Song) {
        self.song = song
        
        songChangeCanceller = $song.sink { newSong in
            
            self.songAttributesChangeCanceller = newSong.objectWillChange.sink { _ in
                self.objectWillChange.send()
            }
        }
    }
}