简体   繁体   中英

SwiftUI List redraw the content of a presented sheet

I'm having an issue with SwiftUI with a presented .sheet when a List content change. Let me explain in detail the situation.

I provided a sample example on Github that you can clone so you can easily reproduce the bug.

Context


I have only 2 views:

  • PlaylistCreationView
  • SearchView

Each of them have respectively their view models called PlaylistCreationViewModel and SearchViewModel .

The PlaylistCreationView has a Add Songs button witch will present the SearchView has a sheet . When the user touch a song in the search results, the SearchView sends a onAddSong completion handler that will notify the PlaylistCreationView that a song has been added.

Here a simple schema of the situation:

情况

Here is also some screenshots of the views:

PlaylistCreationView

播放列表CreationView

SearchView

搜索视图

Problem

When the user touch a song in the SearchView , the SearchView content is redrawn. I mean the view is recreated. The view appears has if it have been loaded for the first time again. Just like this:

空白搜索

To simplify the problem, here is another schema of the situation:

问题模式

Here is what I can observe just after touching a song:

在此处输入图像描述

As you can see, the song has been added to the playlist correctly, but the SearchView is now blank because it has been recreated.

What I can't explain, is why the change of a list in the PlaylistCreationViewModel leads to redrawing the presented sheet .

How to reproduce the issue


I provided a sample example on Github that you can clone. To reproduce the issue:

  1. Clone the project and run it on any iPhone simulator.
  2. Touch "Add song".
  3. Type something on the search bar at the top of the search view.
  4. Select a song. Your search should have disappeared because the view has been rebuilded.

Code


PlaylistCreationViewModel

final class PlaylistCreationViewModel: ObservableObject {
    
    @Published var sheetType: SheetType?
    @Published var songs: [String] = []
    
}

extension PlaylistCreationViewModel {
    
    enum SheetType: String, Identifiable {
        
        case search
        
        var id: String {
            rawValue
        }
        
    }
    
}

PlaylistCreationView

struct PlaylistCreationView: View {
    
    @ObservedObject var viewModel: PlaylistCreationViewModel
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.songs, id: \.self) { song in
                    Text(song)
                }
                Button("Add Song") {
                    viewModel.sheetType = .search
                }
                .foregroundColor(.accentColor)
            }
            .navigationTitle("Playlist")
        }
        .sheet(item: $viewModel.sheetType) { _ in
            sheetView
        }
    }
    
    private var sheetView: some View {
        let addingMode: SearchViewModel.AddingMode = .playlist { addedSong in
            // This line leads SwiftUI to rebuild the SearchView
            viewModel.songs.append(addedSong)
        }
        let sheetViewModel: SearchViewModel = SearchViewModel(addingMode: addingMode)
        return NavigationView {
            SearchView(viewModel: sheetViewModel)
                .navigationTitle("Search")
        }
    }
    
}

SearchViewModel

final class SearchViewModel: ObservableObject {
    
    @Published var search: String = ""
    let songs: [String] = ["Within", "One more time", "Veridis quo"]
    let addingMode: AddingMode
    
    init(addingMode: AddingMode) {
        self.addingMode = addingMode
    }
    
}

extension SearchViewModel {
    
    enum AddingMode {
        
        case playlist(onAddSong: (_ song: String) -> Void)
        
    }
    
}

SearchView

struct SearchView: View {
    
    @ObservedObject var viewModel: SearchViewModel
    
    var body: some View {
        VStack {
            TextField("Search", text: $viewModel.search)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            List(viewModel.songs, id: \.self) { song in
                Button(song) {
                    switch viewModel.addingMode {
                    case .playlist(onAddSong: let onAddSong):
                        onAddSong(song)
                    }
                }
            }
        }
    }
    
}

I'm pretty sure it is some SwiftUI stuff that I'm missing. But I can't get it. I didn't encountered such a case before. I couldn't find anything on the web.

Any help or advice would be really appreciated. Thanks in advance to anyone who'll take the time to read and understand the problem. I hope I've described the problem well enough.

The Problem

When you tap on Songs in SearchView you are invoking a closure block, that is defined inside Sheet view. The code below-:

private var sheetView: some View {
    let addingMode: SearchViewModel.AddingMode = .playlist { addedSong in
        // This line leads SwiftUI to rebuild the SearchView
        viewModel.songs.append(addedSong)
    }
    let sheetViewModel: SearchViewModel = SearchViewModel(addingMode: addingMode)
    return NavigationView {
        SearchView(viewModel: sheetViewModel)
            .navigationTitle("Search")
    }
}

The goal of this closure is to add selected song in Songs array, present in PlaylistCreationViewModel class. Below is that class-:

final class PlaylistCreationViewModel: ObservableObject {
    
    @Published var sheetType: SheetType?
    @Published var songs: [String] = []
    
}

As you can see this class is ObservableObject , and your songs array is @Published property, as soon as you add a song to this array, any class that is using this model and marked with @ObservedObject will refresh its body .In your case that class is below-:

struct PlaylistCreationView: View {
    
    @ObservedObject var viewModel: PlaylistCreationViewModel
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.songs, id: \.self) { song in
                    Text(song)
                }
                Button("Add Song") {
                    viewModel.sheetType = .search
                }
                .foregroundColor(.accentColor)
            }
            .navigationTitle("Playlist")
        }
        .sheet(item: $viewModel.sheetType) { _ in
            sheetView
        }
    }
    
    private var sheetView: some View {
        let addingMode: SearchViewModel.AddingMode = .playlist { addedSong in
            // This line leads SwiftUI to rebuild the SearchView
            viewModel.songs.append(addedSong)
        }
        let sheetViewModel: SearchViewModel = SearchViewModel(addingMode: addingMode)
        return NavigationView {
            SearchView(viewModel: sheetViewModel)
                .navigationTitle("Search")
        }
    }

Now when this view gets refreshed after adding a song it will also reload sheetView view, why? because your sheet is reading its state from viewModel.sheetType whose values is still set to .search

Now, when sheet is called, it also create SearchViewModel objects again, and give it to SearchView , So you are getting complete new Object with empty search string. Below is that model class-:

final class SearchViewModel: ObservableObject {
    
    @Published var search: String = ""
    let songs: [String] = ["Within", "One more time", "Veridis quo"]
    let addingMode: AddingMode
    
    init(addingMode: AddingMode) {
        self.addingMode = addingMode
    }
    
}

Solution-:

You can move addingMode and sheetViewModel property outside of sheetView , and put them inside PlaylistCreationView just above body property. This way when sheet is refreshed your object wouldn't be instantiated from scratch every time.

Working Code-:

mport SwiftUI

struct PlaylistCreationView: View {
    
    @ObservedObject var viewModel: PlaylistCreationViewModel
    var sheetViewModel: SearchViewModel
    
    init(viewModel:PlaylistCreationViewModel) {
        _viewModel = ObservedObject(wrappedValue: viewModel)
         sheetViewModel = SearchViewModel(addingMode: .playlist(onAddSong: {  addSong in
            viewModel.songs.append(addSong)
        }))
        
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.songs, id: \.self) { song in
                    Text(song)
                }
                Button("Add Song") {
                    viewModel.sheetType = .search
                }
                .foregroundColor(.accentColor)
            }
            .navigationTitle("Playlist")
        }
        .sheet(item: $viewModel.sheetType) { _ in
            sheetView
        }
    }
    
    private var sheetView: some View {
         NavigationView {
            SearchView(model: sheetViewModel)
                .navigationTitle("Search")
        }
    }
    
}

SearchView-:

import SwiftUI

struct SearchView: View {
    
    @ObservedObject var viewModel: SearchViewModel
    
    init(model:SearchViewModel) {
        _viewModel = ObservedObject(wrappedValue: model)
    }
    
    var body: some View {
        VStack {
            TextField("Search", text: $viewModel.search)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            List(viewModel.songs, id: \.self) { song in
                Button(song) {
                    switch viewModel.addingMode {
                    case .playlist(onAddSong: let onAddSong):
                        onAddSong(song)
                    }
                }
            }
        }
    }
    
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM