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)
}
})
}
}
}
Post
Replies
Boosts
Views
Activity
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))
}
}
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) }
}
}
}
I posted it to Feedback Asistant. FB11577921
It seems the issue is fixed with Xcode 14.1 beta 3 (14B5033e) / iOS 16.1 beta(20B5056e)
Still has this issue in Xcode Version 14.1 beta 3 (14B5033e), iOS 16.1 beta(20B5056e)
I'm having the same issue on iOS 16.0 prod.
In my environment, AVSpeechUtterance.pitchMultiplier is applied to AVSpeechSynthesizer, but .rate isn't applied.
Hi, I found other problem with NavigationLink.disabled(_ :Bool) not working on iOS16.
I put NavigationLink into HStack to try it out and disabled worked.(But I cannot explain why this works.)
It seems that onAppear is always called in the following code.
I hope this is helpful.
struct ContentView: View {
@State var onAppeared: Set<Int> = .init()
var body: some View {
List() {
ForEach(0...100, id: \.self) { num in
HStack {
NavigationLink {
Text("\(num)")
} label: {
Cell(num: num)
.onAppear() {
NSLog("onAppear: \(num), count: \(onAppeared.count)")
onAppeared.insert(num)
}
}
}
}
}
}
}
struct Cell: View {
let num: Int
var body: some View {
Text("label: \(num)")
}
}
I received a message from Apple's Feedback that this issue has been resolved. It did not reappear in Xcode 14.2(14C18) / iOS 16.2.
I created a ViewModifier to work around this problem.
If you are not using NavigationPath, NavigationDestinationSelectedViewModifier may be available.
https://github.com/hmuronaka/NavigationDestinationSelectedViewModifier
Example
struct ContentView: View {
var body: some View {
NavigationLink("SubView") {
SubView()
}
}
}
struct SubView: View {
@State private var selection: Int?
var body: some View {
List(selection: $selection) {
ForEach(0...10, id: \.self) { idx in
NavigationLink(value: idx) {
Text("Detail \(idx)")
}
}
}
.navigationDestination(selected: $selection, destination: { value in
DetailView(value: value)
})
}
}
struct DetailView: View {
let value: Int
var body: some View {
Text("\(value)")
.navigationTitle("Detail")
}
}
@nteissler
Your comment helped me to calm down.
I have encountered bugs in NavigationView and NavigationStack, such as those described in the following links, multiple times,
which has made it difficult for me to remain calm.
https://developers.apple.com/forums/thread/715589
https://developers.apple.com/forums/thread/715970
https://developers.apple.com/forums/thread/693137
I am currently trimming down the production code and preparing the reproduction code.
I have figured out the situations when the issue occurs and when it doesn't in the production code,
but I have not yet reached the point of having a reproduction code.
In the production code, an infinite loop occurs on iOS 16.4 when referencing a StateObject in navigationDestination.
Without referencing the StateObject, the infinite loop does not occur.
I have attached the reproducible code that I am currently working on below, with the relevant part marked with a 🌟.
Of course, there is a possibility that there is a bug in my code.
However, I cannot understand why an infinite loop occurs depending on whether or not a StateObject is referenced.
Furthermore, since the issue cannot be reproduced in the reproducible code, it may not be the root cause of the bug.
I am still working on creating the reproducible code, and I will share the details as soon as I know more.
enum Kind { case none, a, b, c }
@MainActor
class ContentModel: ObservableObject {
@Published var kind: Kind = .a
@Published var vals: [Selection] = {
return (1...5).map { Selection(num: $0) }
}()
}
// Selection is storing the selected values in the NavigationStack.
struct Selection: Hashable, Identifiable {
let id = UUID()
let num: Int
}
// Data is corresponding to the selection.
struct Data {
let data: Int
}
struct ContentView: View {
@StateObject var model: ContentModel = .init()
@State var selection: Selection?
@State var data: Data?
var body: some View {
list
// Convert selection into data.
.onChange(of: selection) { newValue in
if let selection {
data = Data(data: selection.num * 10)
} else {
data = nil
}
}
}
private var list: some View {
List(selection: $selection) {
ForEach(model.vals) { val in
NavigationLink(value: val) {
Text("\(String(describing: val))")
}
}
}
// In production code, this navigationDestination is defined as a Custom ViewModifier.
.navigationDestination(isPresented: .init(get: {
return data != nil
}, set: { newValue in
if !newValue {
data = nil
}
}), destination: {
// 🌟 If the StateObject is referenced here, the destination will end up in an infinite loop.
// (This code has not yet reached the point of reproducing the issue, so it wont cause an infinite loop yet.)
SubView(kind: model.kind)
// If the StateObject is not referenced, it will transition to the SubView normally.
// SubView()
})
.onChange(of: selection) { newValue in
if newValue == nil && data != nil {
data = nil
}
}
}
}
//
struct SubView: View {
init(kind: Kind) {
}
init() {
}
var body: some View {
Text("Content")
}
}
I have created a reproduction code.
However, this issue cannot be reproduced with just this code.
In my case, it occurs only in the iOS 16.4 environment when Google AdMob is installed via CocoaPods.
In this case, the 🌟 SubView gets caught in an infinite loop regardless of which SubView is called.
If you comment out the 🌟🌟 "@Environment(.dismiss) private var dismiss" line, it seems to work properly.
Additionally, this code worked correctly on the iOS 16.0 and iOS 16.2 simulators.
I am not sure if the cause of this problem lies in SwiftUI iOS 16.4 or Google AdMob
(or if it's a significant misunderstanding on my part), but I hope for a prompt resolution.
Here is the environment I tested in:
Xcode Version 14.3 (14E222b)
CocoaPods: 1.11.3, 1.12.0
AdMob: 9.14.0, 10.3.0
import SwiftUI
@main
struct iOS16_4NavigationSample2App: App {
var body: some Scene {
WindowGroup {
NavigationStack {
NavigationLink {
ContentView()
} label: {
Text("Content")
}
}
}
}
}
enum Kind { case none, a, b, c }
struct Value: Hashable, Identifiable {
let id: UUID = UUID()
var num: Int
}
@MainActor
class ContentModel: ObservableObject {
@Published var kind: Kind = .a
@Published var vals: [Value] = {
return (1...5).map { Value(num: $0) }
}()
}
struct ContentView: View {
@StateObject private var model = ContentModel()
@State private var selectedData: Value?
// 🌟🌟
@Environment(\.dismiss) private var dismiss
init() {
}
var body: some View {
List(selection: $selectedData) {
ForEach(model.vals) { val in
NavigationLink(value: val) {
Text("1")
}
}
}
.navigationDestination(isPresented: .init(get: {
selectedData != nil
}, set: { val in
if !val {
selectedData = nil
}
}), destination: {
// 🌟
SubView(kind: model.kind)
// SubView()
})
}
}
struct SubView: View {
init(kind: Kind) {
print("init(kind:)")
}
init() {
print("init")
}
var body: some View {
Text("Content")
}
}
# Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '16.0'
target 'iOS16_4NavigationSample2' do
use_frameworks!
pod 'Google-Mobile-Ads-SDK'
end
After modifying the ContentView as shown below, the transition to the SubView now works.
(🌟 indicates the modified parts)
However, since the cause of the issue is unknown, I cannot judge whether this is an appropriate workaround or not.
struct ContentView: View {
@StateObject private var model = ContentModel()
@State private var selectedData: Value?
// 🌟
@State private var isShowingSubView = false
@Environment(\.dismiss) private var dismiss
init() {
}
var body: some View {
List(selection: $selectedData) {
ForEach(model.vals) { val in
NavigationLink(value: val) {
Text("\(val.num)")
}
}
}
// 🌟
.onChange(of: selectedData, perform: { newValue in
// In production code, convert data here.
isShowingSubView = newValue != nil
})
.navigationDestination(isPresented: $isShowingSubView, destination: {
SubView(kind: model.kind)
// SubView()
})
.onChange(of: isShowingSubView) { newValue in
if !newValue && selectedData != nil {
selectedData = nil
}
}
}
}
I've posted feedback. FB12552709
Here is a workaround for this issue in the reproduction code.
struct SampleSection: View {
@State private var isLoaded = false
var body: some View {
let _ = Self._printChanges()
if !isLoaded {
Section("Header") {}
.hidden()
.onAppear {
NSLog("SampleSection onAppear.")
isLoaded = true
}
} else {
Section("Header") {
Text("Text")
}
.onDisappear() {
NSLog("Sample Section onDisappear.")
isLoaded = false
}
}
}
}