ios - SwiftUI List 重绘已呈现工作表的内容
问题描述
.sheet
当List
内容更改时,我遇到了 SwiftUI 的问题。让我详细解释一下情况。
我在 Github 上提供了一个示例示例,您可以克隆该示例,以便轻松重现该错误。
语境
我只有 2 意见:
PlaylistCreationView
SearchView
他们每个人都有各自的视图模型,称为PlaylistCreationViewModel
和SearchViewModel
。
有PlaylistCreationView
一个Add Songs
按钮女巫将呈现SearchView
有一个sheet
。当用户在搜索结果中触摸一首歌曲时,SearchView
发送onAddSong
完成处理程序将通知PlaylistCreationView
歌曲已添加。
这里有一个简单的情况示意图:
这里还有一些视图的截图:
播放列表CreationView
搜索视图
问题
当用户触摸 中的歌曲时SearchView
,SearchView
内容会被重绘。我的意思是重新创建视图。如果第一次再次加载该视图,则会显示该视图。像这样:
为了简化问题,这里是另一种情况的模式:
这是我在触摸一首歌后可以观察到的:
如您所见,歌曲已正确添加到播放列表中,但SearchView
现在为空白,因为它已被重新创建。
我无法解释的是,为什么更改列表中的内容PlaylistCreationViewModel
会导致重新绘制呈现的sheet
.
如何重现问题
我在 Github 上提供了一个示例示例,您可以克隆它。要重现该问题:
- 克隆项目并在任何 iPhone 模拟器上运行它。
- 触摸“添加歌曲”。
- 在搜索视图顶部的搜索栏中输入内容。
- 选择一首歌曲。您的搜索应该已经消失,因为视图已被重建。
代码
播放列表CreationViewModel
final class PlaylistCreationViewModel: ObservableObject {
@Published var sheetType: SheetType?
@Published var songs: [String] = []
}
extension PlaylistCreationViewModel {
enum SheetType: String, Identifiable {
case search
var id: String {
rawValue
}
}
}
播放列表CreationView
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 东西。但我无法得到它。我以前没有遇到过这样的情况。我在网上找不到任何东西。
任何帮助或建议将不胜感激。提前感谢任何愿意花时间阅读和理解问题的人。我希望我已经足够好地描述了这个问题。
解决方案
问题
当您点击歌曲时,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
都将刷新其主体。在您的情况下,该类如下:
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
,因此您将获得带有空搜索字符串的完整新对象。以下是该模型类-:
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
}
}
解决方案-:
您可以在 之外移动addingMode
和sheetViewModel
属性sheetView
,并将它们放在 body属性PlaylistCreationView
上方。这样,当刷新工作表时,您的对象就不会每次都从头开始实例化。
工作代码-:
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")
}
}
}
搜索视图-:
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)
}
}
}
}
}
}