SwiftUI List crashes when removing a selected item - swiftui

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. :-)

Related

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: Updating an array item does not update the child UI immediately

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.

SwiftUI: Reordering with Section crashes app

Goal: Multiple text views visually separated much like what Section{} offers, while also being able to rearrange the items in the list during edit mode. (I am not 100% set on only using section but I haven't found a way to visually distinguish with Form or List.)
The issue: The app crashes on the rearrange when using Section{}.
Error Message: 'Invalid update: invalid number of rows in section 2. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Code:
struct SingleItem: Identifiable {
let id = UUID()
let item: String
}
class ItemGroup: ObservableObject{
#Published var group = [SingleItem]()
}
struct ContentView: View {
#ObservedObject var items = ItemGroup()
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView{
Form {
Button("Add Item"){
addButton()
}
ForEach(Array(items.group.enumerated()), id: \.element.id) { index, item in
Section{
Text(items.group[index].item)
}
}
.onMove(perform: onMove)
}
.navigationBarTitle("List")
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, $editMode)
}
}
func addButton() {
let newItem = SingleItem(item: "Word - \(items.group.count)")
self.items.group.append(newItem)
}
private func onMove(source: IndexSet, destination: Int) {
items.group.move(fromOffsets: source, toOffset: destination)
}
}
Use instead .indices. Tested as worked with no crash on Xcode 12 / iOS 14.
Form {
ForEach(items.indices, id: \.self) { i in
Section {
Text(items[i].title)
}
}
.onMove(perform: onMove)
}

Swiftui WatchKit List with onDelete and confirm alert does not update tableview correctly

Can someone point me in the right direction on how to implement a simple list with a confirm remove-function or at least show best practice here.
Below code will work if the alert-part is removed and delete action is immediate.
Somehow, the presentation of the confirmation-alert makes the the deletion act on the wrong list of persons. First removal also gives a console warning:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.
However, I have no idea on how to solve this without removing the alert. By the way, this exact code worked a couple of weeks ago before my mac updated xcode i believe.
import Foundation
import SwiftUI
import Combine
struct Person: Identifiable{
var id: Int
var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
class People: ObservableObject{
#Published var people: [Person]
init(){
self.people = [
Person(id: 1, name:"One"),
Person(id: 2, name:"Two"),
Person(id: 3, name:"Three"),
Person(id: 4, name:"Four")]
}
}
struct ContentView: View {
#ObservedObject var mypeople: People = People()
#State private var showConfirm = false
#State private var idx = 0
func setDeletIndex(at idxs:IndexSet) {
self.showConfirm = true
self.idx = idxs.first!
}
func delete() {
self.mypeople.people.remove(at: idx)
}
var body: some View {
VStack {
List {
Text("Currently \(mypeople.people.count) persons").font(.footnote)
.alert(isPresented: $showConfirm) {
Alert(title: Text("Delete"), message: Text("Sure?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Delete")) {
self.delete()
})
}
ForEach(mypeople.people){ person in
Text("\(person.name)")
}.onDelete { self.setDeletIndex(at: $0) }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The problem is due to conflict of updating alert closing & List record removing. The working workaround is to delay deleting, as below (tested with Xcode 11.4)
Text("Currently \(mypeople.people.count) persons").font(.footnote)
.alert(isPresented: $showConfirm) {
Alert(title: Text("Delete"), message: Text("Sure?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Delete")) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { // here !!
self.delete()
}
})
}

SwiftUI Form Picker only shows once

I am playing around with SwiftUI, and am currently building a Form using a Picker.
import SwiftUI
struct ContentView: View {
private let names = ["Bill", "Peter", "Johan", "Kevin"]
#State private var favoritePerson = "Bill"
var body: some View {
NavigationView {
Form {
Picker("Favorite person", selection: $favoritePerson) {
ForEach(names, id: \.self) { name in
Text(name)
}
}
}
.navigationBarTitle("Form", displayMode: .inline)
}
}
}
The first time you tap on the "Favorite person" row, the picker shows up fine, and tapping on one of the names brings you back to the form. But tapping on the form row a second time doesn't do anything: you don't go to the picker, the row stays highlighted but nothing happens. If this is a SwiftUI bug, is there a known workaround? (I already needed to use a small navigation bar title to work around the Picker UI bug where otherwise its content moves up when it's shown ☹️)
this issue is just one with the simulator. If you build the app on a physical iOS device, it no longer becomes an issue. It's like that bug with Navigation Link that would only work once.
I have the same problem in Xcode 11.4 and also in the real device.
Picker change didn't call CreateTab, it only worked in initialize.
Picker("Numbers", selection: $selectorIndex) {
ForEach(0 ..< formData.tabs.count) { index in
Text(formData.tabs[index].name).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.selectorIndex].publisher.first()) { (value) in
print(value)
CreateTab(tabs: formData.tabs, index: self.selectorIndex)
}