I have a problem with Navigation View hierarchy.
All screens in my app use the same ViewModel.
When a screen inside navigation link updates the ViewModel (here it is called DataManager), the navigation view automatically goes back to the first screen, as if the "Back" button was pressed.
I tried to shrink my code as much as I could
struct DataModel: Identifiable, Codable {
var name: String
var isPinned: Bool = false
var id: String = UUID().uuidString
}
class DataManager: ObservableObject {
@Published private(set) var allModules: [DataModel]
var pinnedModules: [DataModel] {
allModules.filter { $0.isPinned }
}
var otherModules: [DataModel] {
allModules.filter { !$0.isPinned }
}
func pinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = true
}
}
func unpinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = false
}
}
static let instance = DataManager()
fileprivate init() {
allModules =
[DataModel(name: "One"),
DataModel(name: "Two"),
DataModel(name: "Three"),
DataModel(name: "Four"),
DataModel(name: "Five")]
}
}
struct ModulesList: View {
@StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.otherModules) { module in
ModulesListCell(module: module)
}
}
}
}
}
fileprivate struct ModulesListCell: View {
let module: DataModel
var body: some View {
NavigationLink {
SingleModuleScreen(module: module)
} label: {
Text(module.name)
}
}
}
}
struct SingleModuleScreen: View {
@State var module: DataModel
@StateObject var dataStorage = DataManager.instance
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(module.name)
.font(.title)
Button {
dataStorage.pinModule(id: module.id)
} label: {
Text("Pin")
}
}
}
}
}
CodePudding user response:
I can guess because when your dataStorage
changed, the ModulesList
will be redrawn, that cause all current ModulesListCell
removed from memory.
Your cells are NavigationLink
and when it destroyed, the navigation stack doesn't keep the screen that's being linked.
I would recommend to watch this wwdc https://developer.apple.com/videos/play/wwdc2021/10022/ and you will know how to manage your view identity properly when your data's changed.
CodePudding user response:
When you tap Pin, your otherModules array is recreated and you do not have the view in Navigation Stack from where you navigated. Thus you are going back automatically, which is desired behaviour. So the solution is Don't destroy your array from where your NavigationLink is created. Make a temporary published array, load Other modules from that array and change the array onAppear like below:
As a workaround which is working in my end:
Add this line in DataManger:
@Published var tempOtherModules:[DataModel] = []
Change your ModulesList like below
struct ModulesList: View {
@StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.tempOtherModules) { module in
ModulesListCell(module: module)
}
}
}.onAppear {
dataStorage.tempOtherModules = dataStorage.otherModules
}
}
}
}