SwiftUI List 重绘已呈现工作表的内容

Posted

技术标签:

【中文标题】SwiftUI List 重绘已呈现工作表的内容【英文标题】:SwiftUI List redraw the content of a presented sheet 【发布时间】:2021-03-02 12:06:24 【问题描述】:

List 内容发生更改时,我遇到了与呈现.sheet 相关的SwiftUI 问题。让我详细解释一下情况。

我提供了您可以克隆的a sample example on Github,以便您轻松重现该错误。

上下文


我只有 2 个视图:

PlaylistCreationView SearchView

他们每个人的视图模型分别称为PlaylistCreationViewModelSearchViewModel

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

这里有一个简单的情景模式:

这里也是一些视图的截图:

播放列表创建视图

搜索视图

问题

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

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

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

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

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

如何重现问题


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

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

代码


播放列表创建视图模型

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


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

播放列表创建视图

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

搜索视图模型

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 的东西。但我无法得到它。我以前没有遇到过这样的情况。我在网上找不到任何东西。

任何帮助或建议将不胜感激。提前感谢任何愿意花时间阅读和理解问题的人。我希望我已经足够好地描述了这个问题。

【问题讨论】:

您必须使用 100% 纸张吗?我的意思是为什么不使用 CustomView 然后所有问题都解决了,因为工作表有点错误,即使有人在您的代码中发现问题,也可以理解错误并处理它,也许新的拯救不需要Xcode 或 SwiftUI 的更新。我 99% 确定这个问题与您的代码无关,而与工作表有关。 @swiftPunk 感谢您的回复。我不必使用床单。但这是在 ios 应用程序之间导航的标准方式,对用户体验来说是可观的。如果这是一个错误,我会向 Apple 报告。但我想确定这不是我自己的错误。 什么是工作表? sheet 只不过是一个显示 View 的动画,你可以通过一些编码来做到这一点,如果不想要代码,那么你需要处理 apple bugy sheet。我对床单有很多经验,他们只是没有准备好充分使用, @swiftPunk 使用工作表对我的主要优势在于我正在构建的多平台应用程序。利用组件的简单性会很棒。现在我必须找到另一个解决方案并将错误报告给 Apple。感谢您的建议。 【参考方案1】:

问题

当您点击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 类中。下面是那个类-:

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

你可以看到这个类是 ObservableObject ,而你的 songs 数组是 @Published 属性,只要你向这个数组中添加一首歌曲,任何使用这个模型并标有 @ObservedObject 的类将刷新其 body 。在您的情况下,该类低于-:

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 视图,为什么?因为您的工作表正在从viewModel.sheetType 读取其状态,其值仍设置为.search

现在,当调用 sheet 时,它还会再次创建 SearchViewModel 对象,并将其提供给 SearchView,因此您将获得完整的 new Object 搜索字符串为空。下面是那个模型类-:

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 之外,并将它们放在PlaylistCreationView 中,就在body 属性上方。这样,当刷新工作表时,您的对象就不会每次都从头开始实例化。

工作代码-:

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)
                    
                
            
        
    
    

【讨论】:

感谢您的详细解答。是的,它有效。我必须在.sheet() 上添加onDismiss 参数,以在每次SearchView 被解除时实例化一个新的SearchViewModel。我不希望视图模型属性更改重新加载所有视图主体。再次感谢

以上是关于SwiftUI List 重绘已呈现工作表的内容的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中触发状态更改/重绘切换更改

滚动视图和 .sheet

SwiftUI 没有使用 List 和 TabView 正确处理 @State

SwiftUI 强制子视图重绘

SwiftUI 工作表在第一次呈现时被关闭

SwiftUI:ObservableObject 在重绘时不会保持其状态