SwiftUI: Updating an array item does not update the child UI immediately - swiftui

I have an array of items (numbers) to be presented to the user using NavigationView, List and a leaf page.
When I update an item (numbers[index] = ...) on a leaf page, it updates the list correctly (which I see when I go back to the list), but not the leaf page itself immediately. I see the change if I go back to the list and re-open the same leaf page.
I would like to understand why it does not update the UI immediately, and how to fix it. Here is the simplified code to re-produce this behavior.
ADDITIONAL INFORMATION
This code works fine on Xcode 12. It fails only on Xcode 12.1 (RC1) and Xcode 12.2 (beta3).
import SwiftUI
struct NumberHolder: Identifiable {
let id = UUID()
let value:Int
}
struct Playground: View {
#State var numbers:[NumberHolder] = [
NumberHolder(value:1),
NumberHolder(value:2),
NumberHolder(value:3),
NumberHolder(value:4),
]
var body: some View {
NavigationView {
List(numbers.indices) { index in
let number = numbers[index]
NavigationLink(destination: VStack {
Text("Number: \(number.value)")
Button("Increment") {
numbers[index] = NumberHolder(value: number.value + 1)
}
} ) {
Text("Number: \(number.value)")
}
}
}
}
}
struct Playground_Previews: PreviewProvider {
static var previews: some View {
Playground()
}
}

Update
Apple has since replied to my Issue stating they have resolved this since watchOS 8 beta 3. I've tested this on WatchOS 9 and iOS 16 and this is indeed now working correctly.
Previous answer:
This had me scratching my day for a few weeks.
It appears there are many features within SwiftUI that do not work in Views that are placed in Lists directly, however if you add a ForEach inside the List said features (such as .listRowPlatterColor(.green) on WatchOS) start to work.
Solution
On iOS 14.2 if you wrap the NavigationLink inside a ForEach the NavigationLink destination (leaf page) will update right away when the data model is updated.
So change your code to
var body: some View {
NavigationView {
List {
ForEach(numbers.indices) { index in
let number = numbers[index]
NavigationLink(destination: VStack {
Text("Number: \(number.value)")
Button("Increment") {
numbers[index] = NumberHolder(value: number.value + 1)
}
} ) {
Text("Number: \(number.value)")
}
}
}
}
}
Frustratingly, this does not solve the issue when using WatchOS, in WatchOS 7.0 the leaf page is updated, however in WatchOS 7.1 (version goes hand in hand with iOS 14.2 that suffered this "issue") so I have an issue open with Apple FB8892330
Further frustratingly, I still don't know if this is a bug or a feature in SwiftUI, none of the documentation state the requirement for ForEach inside of Lists

Try this one. I tested and it works.
import SwiftUI
struct NumberHolder: Identifiable {
let id = UUID()
let value:Int
}
struct ContentView: View {
#State var numbers:[NumberHolder] = [
NumberHolder(value:1),
NumberHolder(value:2),
NumberHolder(value:3),
NumberHolder(value:4),
]
var body: some View {
NavigationView {
List(numbers.indices) { index in
NavigationLink(destination: DetailView(numbers: $numbers, index: index)) {
Text("Number: \(self.numbers[index].value)")
}
}
}
}
}
struct DetailView: View {
#Binding var numbers:[NumberHolder]
let index: Int
var body: some View {
VStack {
Text("Number: \(self.numbers[index].value)")
Button("Increment") {
numbers[index] = NumberHolder(value: numbers[index].value + 1)
}
}
}
}

I found the answer. It was a bug in Xcode.
This code (without any changes) works fine under Xcode 12.0, but fails to update under Xcode 12.2 beta 2.

Related

AsyncImage in List is broken on iOS 16

struct Item: Identifiable {
let id: String
let url = URL(string: "https://styles.redditmedia.com/t5_j6lc8/styles/communityIcon_9uopq0bazux01.jpg")!
}
struct Content: View {
let model: [Item] = {
var model = [Item]()
for i in 0 ..< 100 {
model.append(.init(id: String(i)))
}
return model
}()
var body: some View {
List(model) { item in
Row(item: item)
}
}
}
struct Row: View {
let item: Item
var body: some View {
AsyncImage(url: item.url)
}
}
Running code above with Xcode 14.1 on iOS 16.1 simulator, AsyncImage sometimes doesn’t properly show downloaded image but grey rectangle instead when scrolling in list. Is this bug or am I missing something? Thanks
My solution was to use VStack in ScrollView instead of List. It looks like it's working and it doesn't have any other drawbacks.

SwiftUI: How to update detail column from a distant child using iOS16/iPadOS16 NavigationSplitView and NavigationLink

I'm trying to update an older app to use the new NavigationSplitView and NavigationLink, but trying to wrap my head around the proper way to do it when the sidebar has a hierarchy of child objects and I want to update the detail view from a distant child object. For example, based on the WWDC2022 example project Navigation Cookbook I tried the following and it didn't work:
TestApp.swift
#main
struct TestApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
NavigationSplitView {
ProjectListView()
} detail: {
if let chapter = appState.chapter {
ChapterDetailView(chapter: chapter)
} else {
Text("Pick a Chapter")
}
}
}
}
}
ChapterListView.swift < A distant (3 levels down) sibling of ProjectListView()
List(selection: $appState.chapter) {
ForEach(chapters) { chapter in
NavigationLink(chapter.title ?? "A Chapter", value: chapter)
}
}
appState.swift
class AppState: ObservableObject {
#Published var chapter: Chapter?
}
I'm sure I'm just not understanding the basics of how the new way of doing navigation works. Yes, I am targeting iOS16
This is a known bug in beta 1. To workaround, the logic in the detail section needs to be wrapped in a ZStack. From the release notes:
Conditional views in columns of NavigationSplitView fail to update on some state changes. (91311311)
Workaround: Wrap the contents of the column in a ZStack. TestApp works if changed to this:
#main
struct TestApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
NavigationSplitView {
ProjectListView()
} detail: {
// Wrap in a ZStack to fix this bug
ZStack {
if let chapter = appState.chapter {
ChapterDetailView(chapter: chapter)
} else {
Text("Pick a Chapter")
}
}
}
}
}
}

SwiftUI - LazyVStack displaying more elements in view than required

I've been following a youtube tutorial for using LazyVStacks.
https://www.youtube.com/watch?v=o6D7mUXjSmI
When I run the same code as per the tutorial, the LazyVStack using Xcode Version 13.2.1 (13C100) on a iPhone 11 Pro Max (running iOS 15.2) prints out 83 statements, when only 42 rows are in view. As per the tutorial it should only print 42 statements to the console.
Is this an Xcode/iOS bug? I'm unable to download the latest Xcode as my Mac doesn't support macOS 12.0 to verify.
Video tutorial showing on 42 print statements in the console on iPhone 11 Pro Max:
My code showing 83 print statements in the console on iPhone 11 Pro Max
import SwiftUI
struct SampleRow: View {
let id: Int
var body: some View {
Text("Row \(id)")
}
init(id: Int) {
print("Loading row \(id)")
self.id = id
}
}
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(1...1000, id: \.self, content: SampleRow.init)
}
}
}
}
I got the exact same problem, I've got several views that render and fetch data dynamically upon reaching the bottom of the page but for some unknown reason, the same exact code is not working on a view where all the data are rendered at once.. I don't know if it's a bug in LazyVStack implementation or anything else but the behavior is ambiguous
It's working fine in Xcode 13. I'm not sure why you need a LazyVStack, unless you're using some sort of grid. I slightly modified the code based on there's no much use for it here. It's printing all 1000.
import SwiftUI
struct SampleRow: View {
let id: Int
var body: some View {
Text("Row \(id)")
}
init(id: Int) {
print("Loading row \(id)")
self.id = id
}
}
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1...1000, id: \.self) { index in
SampleRow(id: index)
}
}
}
}

Why the selected rows don't stay selected after scrolling in SwiftUI?

Why the selected rows become unselected if the list is scrolled (See the pictures)? Xcode 12.2. iOS 14.2.
I also get a console message:
[Assert] Attempted to call -cellForRowAtIndexPath: on the table view
while it was in the process of updating its visible cells, which is
not allowed.
Update
This seems to be iOS 14.2 bug. I downloaded a simulator for iOS 14.1 version and everything works just fine.
import SwiftUI
struct ContentView: View {
#State private var selectedRows = Set<String>()
var items = ["item1", "item2", "item3", "item4","item5","item6","item7","item8","item9","item10","item11","item12","item13","item14","item15","item16","item17","item18","item19","item20","item21","item22"]
var body: some View {
NavigationView {
List(selection: $selectedRows) {
ForEach(items, id: \.self) { item in
Text(item)
}
}
.listStyle(InsetGroupedListStyle())
.environment(\.editMode, Binding.constant(.active))
}
}
}

SwiftUI List crashes when removing a selected item

I have a severe problem with the SwiftUI List view in Xcode 12 (beta) (MacOS App).
When a List item, which is selected, is removed, the List crashes every time.
"[General] Row 2 out of row range [0-1] for rowViewAtRow:createIfNeeded:"
Looks like a bug in SwiftUI to me. What can I do to prevent the crash? I've tried several things already, but with no success.
Example code:
//
// Example to reproduce bug
// * Select no item or other than last item and press button: selection is reset, last item is removed, no crash
// * Select last list item and press button "Delete last item" => Crash
//
import SwiftUI
class MyContent: ObservableObject {
#Published var items: [String] = []
#Published var selection: Set<String> = Set()
init() {
for i in 1...5 {
self.items.append(String(i))
}
}
}
struct MyView: View {
#ObservedObject var content: MyContent = MyContent()
var body: some View {
VStack {
List(content.items, id: \.self, selection: $content.selection) {
item in
Text("\(item)")
}
Button("Delete last item", action: {
if content.items.count > 0 {
content.selection = Set() // reset selection
var newItems = Array(content.items)
newItems.removeLast()
content.items = newItems
}
})
}
}
}
Re-tested after installing MacOS 11.0 Beta 7 (20A5374g).
The example code doesn’t crash any more, the bug seems to be fixed.
Thanks to all for testing and so giving me the hint, that it's a MacOS beta bug. :-)