SwiftUI List重绘呈现的内容sheet

SwiftUI List redraw the content of a presented sheet

List 内容发生变化时,我在使用 SwiftUI 时遇到问题,显示 .sheet。让我详细说明一下情况。

我提供了 a sample example on Github 你可以克隆,这样你就可以轻松地重现错误。

上下文


我只有 2 次观看:

他们每个人都有各自的视图模型 PlaylistCreationViewModelSearchViewModel

PlaylistCreationView 有一个 Add Songs 按钮 将显示 SearchView 有一个 sheet。当用户触摸搜索结果中的歌曲时,SearchView 发送一个 onAddSong 完成处理程序,通知 PlaylistCreationView 一首歌曲已添加。

这里是一个简单的情况示意图:

这里还有一些观点的截图:

PlaylistCreationView

搜索视图

问题

当用户触摸 SearchView 中的歌曲时,SearchView 内容将被重绘。我的意思是重新创建视图。如果视图是第一次再次加载,则会出现该视图。就像这样:

为了简化问题,这里是另一种情况的模式:

这是我在触摸一首歌后可以观察到的:

如您所见,歌曲已正确添加到播放列表中,但SearchView现在是空白的,因为它已被重新创建。

我无法解释的是,为什么 PlaylistCreationViewModel 中的列表更改导致重新绘制呈现的 sheet

如何重现问题


我提供了 a sample example on Github 你可以克隆。要重现问题:

  1. 克隆项目并在任何 iPhone 模拟器上 运行 它。
  2. 触摸“添加歌曲”。
  3. 在搜索视图顶部的搜索栏中键入内容。
  4. Select一首歌。您的搜索应该已经消失,因为视图已经重建。

代码


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)
        
    }
    
}

搜索视图

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)
                    }
                }
            }
        }
    }
    
}

我很确定我缺少一些 SwiftUI 的东西。但我无法得到它。我以前没有遇到过这样的情况。我在网上找不到任何东西。

如有任何帮助或建议,我们将不胜感激。提前感谢任何愿意花时间阅读和理解问题的人。我希望我已经充分描述了问题。

问题

当您点击 SearchView 中的歌曲时,您正在调用 closure 块,该块在 Sheet 视图中定义。下面的代码-:

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")
    }
}

这个闭包的目标是将选定的歌曲添加到 Songs 数组中,出现在 PlaylistCreationViewModel class 中。下面是 class-:

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

如您所见,class 是 ObservableObject,而您的 songs 数组是 @Published 属性,只要您将歌曲添加到这个数组,任何使用这个模型并标有 @ObservedObject 的 class 都会刷新它的 body 。在你的情况下 class 如下:

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")
        }
    }

现在,当添加 歌曲 后刷新此视图时,它也会重新加载 sheetView 视图,为什么?因为您的 sheet 正在从 viewModel.sheetType 读取其状态,其值仍设置为 .search

现在,当 sheet 被调用时,它还会再次创建 SearchViewModel 个对象,并将其交给 SearchView,所以你得到了完整的 new 对象 与空搜索字符串。下面是模型 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
    }
    
}

解决方法-:

您可以将 addingModesheetViewModel 属性 移到 sheetView 之外,然后将它们放在 PlaylistCreationViewbody 属性。这样,当 sheet 刷新时,您的对象不会每次都从头开始实例化。

工作代码-:

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)
                    }
                }
            }
        }
    }
    
}