It seems the issue is fixed with Xcode 14.1 beta 3 (14B5033e) / iOS 16.1 beta(20B5056e)
Post
Replies
Boosts
Views
Activity
I posted it to Feedback Asistant. FB11577921
If you simply want to be able to recurse, the following code might work.
NavigationStack {
RecursiveView (rootItemList)
.navigationDestination(for: Item.self) { item in
RecursiveView(item.childList)
}
}
struct RecursiveView (itemList) {
List {
ForEach (item in itemList) {
if item.isLeaf {
someView (item)
} else {
NavigationLink(value: item) { label(item) }
}
}
}
Hi, I wanted to detect that a NavigationLink was selected, so I wrote a ViewModifier using list($selection:) and navigationDestination(isPresent:).
If you are not using $path in your NavigationStack(path:), you might be able to use this ViewModifier idea for data loading with onChange(of: selection) {}.
I attach below the sample code for a recursive FileListViewer.
You can get this ViewModifier and other sample code from https://github.com/hmuronaka/NavigationDestinationSelectedViewModifier.
I hope this idea will be helpful to you.
import SwiftUI
import NavigationDestinationSelectedViewModifier
struct PlainFileList2<Destination: View>: View {
let current: URL
let paths: [URL]
@ViewBuilder let destination: (URL) -> Destination
@State private var selection: URL?
@State private var childPaths: [URL]?
var body: some View {
List(selection: $selection) {
ForEach(paths, id: \.self) { url in
if FileManager.default.isDirectory(url: url) {
NavigationLink(value: url) {
Label(url.lastPathComponent, systemImage: "folder")
}
} else {
self.destination(url)
}
}
}
.navigationDestination(selection: $selection, item: $childPaths ) { childPaths in
if let selection {
PlainFileList2(current: selection, paths: childPaths, destination: self.destination)
}
}
.onChange(of: selection, perform: { newValue in
if let newValue, FileManager.default.isDirectory(url: newValue) {
self.childPaths = try! FileManager.default.contentsOfDirectory(at: newValue, includingPropertiesForKeys: [.parentDirectoryURLKey, .creationDateKey, .fileSizeKey], options: [])
} else {
self.childPaths = nil
}
})
.navigationTitle(current.lastPathComponent)
}
}
ViewModifier
import SwiftUI
fileprivate struct NavigationDestinationViewModifier<SelectionValue: Hashable, Value: Equatable, Destination: View>: ViewModifier {
@Binding var selection: SelectionValue?
@Binding var item: Value?
@ViewBuilder let destination: (Value) -> Destination
func body(content: Content) -> some View {
content
.navigationDestination(isPresented: .init(get: {
item != nil
}, set: { newValue in
if !newValue {
item = nil
}
})) {
if let selected = item {
destination(selected)
} else {
EmptyView()
}
}
.onChange(of: item) { newValue in
if newValue == nil && selection != nil {
selection = nil
}
}
}
}
public extension View {
func navigationDestination<SelectionValue: Hashable, Value: Equatable, Destination: View>(selection: Binding<SelectionValue?>, item: Binding<Value?>, @ViewBuilder destination: @escaping (Value) -> Destination) -> some View {
return self.modifier(NavigationDestinationViewModifier(selection: selection, item: item, destination: destination))
}
}
Hi, I had same problem.
I worked around the problem by forcibly re-rendering the list.
(But the list may flicker when redrawing.)
I’ve marked the changes in the original sample code with 🌟.
I hope the problem will be fixed iOS 16.1...
Outline
Hide the list before updating data.
Show the list from DispatchQueue.main.async when data is updated.
Scroll by list.onAppear or list.onChange
import SwiftUI
///A simple data model for the demo. Only stores an UUID.
struct DataModel: Identifiable, Hashable {
let id: UUID = UUID()
var nameUUID: String {
id.uuidString
}
}
struct ContentView: View {
///Array with some data to show
@State private var data: [DataModel] = []
///Selected row
@State private var selection: DataModel?
// 🌟 In some situations, the initial value should be true.
@State private var isHidingList = false
var body: some View {
VStack(alignment: .leading) {
HStack {
//Create a new array for showing in the list.
//This array will be bigger than the last one.
//The selection will be the last element of the array (triggering the bug)
Button {
//Increment the size of the new List by 5
let numberElements = data.count + 5
//Create a new Array of DataModel with more 5 elements that the previous one
let newData = (0 ..< numberElements).map { _ in DataModel() }
//Select the last element of the array/list.
//This will make sure that the scrollTo will go to the end
let newSelection = newData.last
// 🌟 1. hide list before updating data.
//Update State for the new values
isHidingList = true
data = newData
selection = newSelection
} label: {
Text("Randomize & Select Last")
}
Spacer()
//Create a new array for showing in the list.
//This array will be bigger than the last one.
//The selection will be the a random element of the array (only triggering the bug when the element is )
Button {
//Increment the size of the new List by 5
//If empty will start with 40 (reducing the odds of triggering the bug)
let numberElements = data.count == 0 ? 40 : data.count + 5
//Create a new Array of DataModel with more 5 elements that the previous one
let newData = (0 ..< numberElements).map { _ in DataModel() }
//Select a random element of the array/list.
//This will scroll if the element is 'inside' the previous list
//Otherwise will crash
let newSelection = newData.randomElement()
// 🌟 1. hide list before updating data.
//Update State for the new values
isHidingList = true
data = newData
selection = newSelection
} label: {
Text("Randomize & Select Random")
}
}
.padding()
// 🌟
//MARK: ScrollViewReader and List
if isHidingList {
list.hidden()
} else {
list
}
}
// 🌟 2. Show the list from DispatchQueue.main.async when the data is updated.
.onChange(of: data) { _ in
DispatchQueue.main.async {
self.isHidingList = false
}
}
}
private var list: some View {
ScrollViewReader {
proxy in
List(data, selection: $selection) {
dataElement in
//Row (is only the UUID for the rows
Text(dataElement.id.uuidString)
.id(dataElement)
.tag(dataElement)
}
// 🌟3. Scroll
.onAppear() {
guard !isHidingList else { return }
if let selection {
proxy.scrollTo(selection)
}
}
// 🌟 This will not be called in this sample.
.onChange(of: data, perform: { newValue in
guard !isHidingList else { return }
if let selection {
proxy.scrollTo(selection)
}
})
}
}
}