Ishmeet
Ishmeet

Reputation: 705

ObservedObject resets when I update the view using a property. Hence it leads to loss of data

I have an issue where the ViewModel would re-initialize when view is updated.

I have 2 views, SongListView and PlayerView which share an object of Player. When player's Playing state is changed (isPlaying == true), the viewModel in SongListView resets and becomes empty array. Due to which, the list on my view becomes empty.

enter image description here

SongListView:

struct SongListView: View {

    @ObservedObject var model: SongListViewModel = SongListViewModel() // This resets when player.isPlaying is set to true

    @ObservedObject var player: Player

    var body: some View {

        List(model.songs, id: \.id) { song in
            Button(action: {
                self.player.play(link: song.link)
            }) {
                TitleRowView(title: song)
            }
        }
        .onAppear {
            self.model.get()
        }
        .navigationBarTitle(Text("Songs"), displayMode: .inline)
    }
}

SongListViewModel:

class SongListViewModel: ObservableObject {

    @Published var songs: [Song] = [Song(id: 2, name: "ish", link: "ishm")] // When I tap the row, the songs var is re-initialized

    func get() {

        guard let url = URL(string: "apiPath") else { return }

        URLSession(configuration: URLSessionConfiguration.default).dataTask(with: url) {data, response, error 
               // Some more code
               self.songs = data
        }.resume()
    }
}

PlayerView:

struct PlayerView: View {

    @ObservedObject var player: Player

    var body: some View {
        HStack {
            Button(action: {
                if self.player.isPlaying {
                    self.player.pause()
                } else {
                    self.player.play()
                }
            }) {
                // This change causes the viewModel to reset to empty array
                if self.player.isPlaying { 
                    Image(systemName: "pause.fill")
                        .resizable()
                } else {
                    Image(systemName: "play.fill")
                        .resizable()
                }
            }
        }
    }
}

Player:

class Player : ObservableObject
{

    @Published var isPlaying: Bool = false

    private var player: AVPlayer?

    // This method is called when the user taps on a row in List
    func play(link: String) {
        guard let url = URL(string: link) else { return }
        let playerItem = AVPlayerItem(url: url)
        player = AVPlayer(playerItem: playerItem)
        player?.play()
        isPlaying = true // If I comment this line, the songs list in viewModel does not changes
    }    
}

Thanks in Advance!

UPDATE: Still doesn't work

struct SongListView: View {

    @ObservedObject var model: SongListViewModel

    var body: some View {
        // View 
    }
}

struct CategoryListView: View {
    var categoryData : [Category]
    @ObservedObject var player: Player

    var body: some View {
        List(categoryData, id: \.id) { category in
            if category.id == 3 {
                NavigationLink(destination: SongListView(model: SongListViewModel(), player: self.player)) {
                    TitleRowView(title: category)
                }
            }

        }
    }
}

Upvotes: 4

Views: 4204

Answers (4)

Leonardo Lovera
Leonardo Lovera

Reputation: 81

Using @StateObject instead of @ObservedObject worked for me.

A property marked as @StateObject will keep its initially assigned ObservedObject instance as long as the view is needed, even when the struct gets recreated by SwiftUI.

This allows you to maintain the state of your ObservedObject data.

Upvotes: 8

danylo.net
danylo.net

Reputation: 263

Use @StateObject instead! ObservedObject seems to be recreating the entire object every time.

Upvotes: 4

Ishmeet
Ishmeet

Reputation: 705

So finally I was able to fix this issue by removing @ObservedObject property wrapper on player: Player. I am not sure why this works. Seems like having more than one ObservedObject in a view causes this problem. Now my code looks like this:

struct SongListView: View {

    @ObservedObject var model: SongListViewModel

    @State var player: Player

    var body: some View {

        List(model.songs, id: \.id) { song in
            Button(action: {
                self.player.play(link: song.link)
            }) {
                TitleRowView(title: song)
            }
        }
        .onAppear {
            self.model.get()
        }
        .navigationBarTitle(Text("Songs"), displayMode: .inline)
    }
}

struct CategoryListView: View {
    var categoryData : [Category]
    @ObservedObject var player: Player
    let viewModel = SongListViewModel()

    var body: some View {
        List(categoryData, id: \.id) { category in

            if category.id == 3 {
                NavigationLink(destination: SongListView(player: self.player).environmentObject(self.viewModel)) {
                    TitleRowView(title: category)
                }
            }

        }
    }
}

Upvotes: 0

Paulw11
Paulw11

Reputation: 114773

SwiftUI views are structs, and therefore, immutable. When you update the state and cause the view to redraw, it actually creates a new instance of the view.

In your SongListView you have

@ObservedObject var model: SongListViewModel = SongListViewModel()

This means that each time your SongListView is re-drawn (which includes any time that player.isPlaying is changed), you are initialising model with a new instance of SongListViewModel.

You should remove the default value and supply the model via a parameter to the initialiser of SongListView -

@ObservedObject var model: SongListViewModel

Upvotes: 6

Related Questions