0

I have a simple Demo App where I present Items in a List with two sections. The first section shows the favourite items, the second section contains the remaining items (which are not favourites).

The odd behaviour occurs if I change the isFav state.

on iPhoneOS:

  • When I select an item the DetailView will appear.
  • if I change the isFav state (toggle) the DetailView will disappear
  • Video

on iPadOS:

  • When I select an item the DetailView will appear.
  • if I change the isFav state (toggle) the DetailView is not disappearing but in the sidebar, the selection [disappears]
  • Video
//
//  ContentView.swift
//  Shared
//
//  Created by Christian on 06.06.21.
//

import SwiftUI

//MARK: - Data Model

struct Item: Identifiable, Equatable, Hashable {
   var id = UUID().uuidString
   var isFav = false
   var text: String
}

struct ItemScoped: Identifiable, Equatable, Hashable {
   var id: String {
      return item.id
   }
   var item: Item
   var index: Int
}

//MARK: Store

class ItemStore: ObservableObject {
   @Published var items = [Item(text: "Item 1"),
                           Item(text: "Item 2"),
                           Item(isFav: true, text: "Item 3"),
                           Item(text: "Item 4")]
   
   func scopedItems(isFav: Bool) -> [ItemScoped] {
      let sItems: [ItemScoped]  = items.compactMap {
         guard let idx = items.firstIndex(of: $0) else { return nil }
         //find(items, $0)
         return ItemScoped(item: $0, index: idx)
      }
      return sItems.filter { $0.item.isFav == isFav }
   }
}

//MARK: - Views

struct ContentView: View {
   
   // usally this is @EnvironmetObject, due to simplicity I put it here
   @StateObject var store: ItemStore = ItemStore()
   
   var body: some View {
      NavigationView {
         List {
            Section(header: Text("Favorites")) {
               ForEach(store.scopedItems(isFav: true)) { scopedItems in
                  NavigationLink(
                     destination: DetailView(item: $store.items[scopedItems.index]),
                     label: {
                        RowView(item: $store.items[scopedItems.index])
                     })
               }
            }
            Section(header: Text("Other")) {
               ForEach(store.scopedItems(isFav: false)) { scopedItems in
                  NavigationLink(
                     destination: DetailView(item: $store.items[scopedItems.index]),
                     label: {
                        RowView(item: $store.items[scopedItems.index])
                     })
               }
            }
         }
         .navigationTitle("Items")
      }
   }
}

// MARK: Row View

/// RowView for item, tapping the text toggle the `isFav` state
struct RowView: View {
   
   @Binding var item: Item
   
   var body: some View {
      Label(
         title: { Text(item.text) },
         icon: { item.isFav ? Image(systemName: "star.fill") : Image(systemName: "star")}
      )
   }
}


// MARK: Detail View

/// DetailView to change item `text` and toggle `isFav` state
struct DetailView: View {
   
   @Binding var item: Item
   
   var body: some View {
      VStack {
         Spacer()
            .frame(height: 20.0)
         TextField("Title", text: $item.text)
            .background(Color.gray.opacity(0.2))
            .padding(10)
         Toggle("is Fav", isOn: $item.isFav.animation())
            .padding()
         Spacer()
      }
      .padding()
   }
}

// MARK: - Preview

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
      ContentView()
   }
}





cedricbahirwe
  • 1,274
  • 4
  • 15
Christian1313
  • 270
  • 3
  • 3
  • This behaviour is not odd, it is actually the expected behaviour, because when `isFav` state is changing the `List` is getting updated, and the favourite item is getting removed from the list. – cedricbahirwe Jun 07 '21 at 08:42
  • @cedricbahirwe it is not removed it is only displayed in a another section. Another example would by if the sorting of the list is based on last input date, then every time a user edit the item in "DetailView" the detail view will disappear. An it can't be expected because in iPadOS it behave differently. – Christian1313 Jun 07 '21 at 09:01

1 Answers1

0

I found a solution using the tag property of the NavigationLink and @SceneStorage property wrapper.

  1. Create a @SceneStorage (persistent state per scene)

     @State private var sceneItemID: String?  
    

    or

     @SceneStorage private var sceneItemID: String?  
    
  2. Add a tag with the unique id of the item to every NavigationLink

    NavigationLink(destination:  DetailView(item: $item),
                   tag: item.id,
                   selection: $sceneItemID,
                   label: {
                        RowView(item: $item)
                   })
    

    Every time the Navigation Link is used the sceneItemID is updated with the tag (item.id in this case).

  3. In the DetailView update the sceneItemID in the .onAppear() modifier.
    This is necessary due to the behaviour during state change of isFav.

Now it is working only on iPad the Sidebar does not correctly display the selection. On macOS and iPhone this works.


//
//  ContentView.swift
//  Shared
//
//  Created by Christian on 06.06.21.
//

import SwiftUI

//MARK: - Data Model

struct Item: Identifiable, Equatable, Hashable {
   var id = UUID().uuidString
   var isFav = false
   var text: String
}

struct ItemScoped: Identifiable, Equatable, Hashable {
   var id: String {
      return item.id
   }
   var item: Item
   var index: Int
}

//MARK: Store

class ItemStore: ObservableObject {
   @Published var items = [Item(id: "uuid01", text: "Item 1"),
                           Item(id: "uuid02", text: "Item 2"),
                           Item(id: "uuid03", isFav: true, text: "Item 3"),
                           Item(id: "uuid04", text: "Item 4")]
   
   /// scope item to sections and keep knowledge of origin index
   func scopedItems(isFav: Bool) -> [ItemScoped] {
      let sItems: [ItemScoped]  = items.compactMap {
         guard let idx = items.firstIndex(of: $0) else { return nil }
         //find(items, $0)
         return ItemScoped(item: $0, index: idx)
      }
      return sItems.filter { $0.item.isFav == isFav }
   }
}


//MARK: - Views

struct ContentView: View {
   
   // usally this is @EnvironmetObject, due to simplicity I put it here
   @StateObject var store: ItemStore = ItemStore()
   
   @SceneStorage("SceneItemSelectionID") private var sceneItemID: String?
   
   var body: some View {
      NavigationView {
         
         List {
            Section(header: Text("Favorites")) {
               ForEach(store.scopedItems(isFav: true)) { scopedItems in
                  NavigationLink(
                     destination: DetailView(item: $store.items[scopedItems.index]),
                     //MARK: !! IMPORTANT: use unique indetifier as tag
                     tag: store.items[scopedItems.index].id,
                     selection: $sceneItemID,
                     label: {
                        RowView(item: $store.items[scopedItems.index])
                     })
               }
            }
            Section(header: Text("Others")) {
               ForEach(store.scopedItems(isFav: false)) { scopedItems in
                  NavigationLink(
                     destination: DetailView(item: $store.items[scopedItems.index]),
                     //MARK: !! IMPORTANT: use unique indetifier as tag
                     tag: store.items[scopedItems.index].id,
                     selection: $sceneItemID,
                     label: {
                        RowView(item: $store.items[scopedItems.index])
                     })
                  
               }
            }
         }
         .listStyle(SidebarListStyle())
         .navigationTitle("Items")
      }
   }
}

// MARK: Row View

/// RowView for item, tapping the text toggle the `isFav` state
struct RowView: View {
   
   @Binding var item: Item
   
   var body: some View {
      Label(
         title: { Text(item.text) },
         icon: { item.isFav ? Image(systemName: "star.fill") : Image(systemName: "star")}
      )
   }
}


// MARK: Detail View

/// DetailView to change item `text` and toggle `isFav` state
struct DetailView: View {
   
   @Binding var item: Item
   
   @SceneStorage("SceneItemSelectionID") private var sceneItemID: String?
   
   var body: some View {
      VStack {
         Spacer()
            .frame(height: 20.0)
         TextField("Title", text: $item.text)
            .background(Color.gray.opacity(0.2))
            .padding(10)
         Toggle("is Fav:", isOn: $item.isFav.animation())
            .padding()
         Spacer()
      }
      .padding()
      .onAppear() {
         //MARK: !! IMPORTANT set scene selction id again
         sceneItemID = item.id
      }
   }
}

// MARK: - Preview

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
      ContentView()
   }
}



Christian1313
  • 270
  • 3
  • 3