Crash when array is empty in iOS 14.3 - ios

I'm trying to show some placeholder data when the array is empty. This works in iOS 13.7 but something has changed in iOS 14.3 so when the last item is deleted you get this crash:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
If I comment out testStore.data.isEmpty and just return the Form I get no crash.
How can I show placeholder when array is empty in iOS 14.3?
struct Test: Identifiable {
var text: String
var id: String { text }
}
extension Test {
final class Store: ObservableObject {
#Published var data = [Test(text: "Hi"), Test(text: "Bye")]
}
}
struct TestList: View {
#EnvironmentObject var testStore: Test.Store
var body: some View {
Group {
if testStore.data.isEmpty {
Text("Empty")
} else {
Form {
ForEach(testStore.data.indices, id: \.self) { index in
TestRow(test: $testStore.data[index], deleteHandler: { testStore.data.remove(at: index) })
}
}
}
}
}
}
struct TestRow: View {
#Binding var test: Test
let deleteHandler: (() -> ())
var body: some View {
HStack {
Text(test.text)
.font(.headline)
Spacer()
Button(action: deleteHandler, label: Image(systemName: "trash"))
}
}
}

You can use the extension proposed here:
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, #ViewBuilder content: #escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
Then, if you also want to keep ForEach instead of List you can do:
struct TestList: View {
#EnvironmentObject var testStore: Test.Store
var body: some View {
Group {
if testStore.data.isEmpty {
Text("Empty")
} else {
Form {
ForEach(testStore.data.indices, id: \.self) { index in
Safe($testStore.data, index: index) { binding in
TestRow(test: binding, deleteHandler: { testStore.data.remove(at: index) })
}
}
}
}
}
}
}

Related

SwiftUI: List does not display the data

I tried to display the data of my server, the items appear and then disappeared a second after what?
Yet I'm displaying a static list that works ..
Look at the start of the video the bottom list:
My code:
struct HomeView: View {
#EnvironmentObservedResolve private var viewModel: HomeViewModel
var test = ["", ""]
var body: some View {
VStack {
Button(
action: {
},
label: {
Text("My Button")
}
)
List(test, id: \.self) { el in
Text("Work")
}
List(viewModel.items) { el in
Text("\(el.id)") // Not work
}
}
.padding()
.onAppear {
viewModel.getData()
}
}
}
My viewModel:
class HomeViewModel: ObservableObject {
private let myRepository: MyRepository
#Published var items: [Item] = []
init(myRepository: MyRepository) {
self.myRepository = myRepository
}
}
#MainActor extension HomeViewModel {
func getData() {
Task {
items = try await myRepository.getData()
}
}
}

Passing #Binding variable from protocol var to if in SwiftUI

I have a model like this:
protocol PurchasableProduct {
var randomId: String { get }
}
class Cart: Identifiable {
var items: [PurchasableProduct]
init(items: [PurchasableProduct]) {
self.items = items
}
}
class Product: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
var notes: String = ""
}
class DigitalGood: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
}
where items conform to protocol PurchasableProduct.
I want to build a View that shows cart like this:
struct CartView: View {
#State var cart: Cart
var body: some View {
List {
ForEach(cart.items.indices) { index in
CartItemView(item: self.$cart.items[index])
}
}
}
}
where CartItemView is:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
Text("Product")
} else {
Text("Digital Good")
}
}
}
}
That's working and give me result as
This (screenshot)
But I want to extend this a but more that my items element can be passed as a binding variable lets say as:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
VStack {
TextField("add notes", text: (self.$item as! Product).notes) // ❌ Cannot convert value of type 'String' to expected argument type 'Binding<String>'
TextField("add notes", text: (self.$item as! Binding<Product>).notes) // ⚠️ Cast from 'Binding<PurchasableProduct>' to unrelated type 'Binding<Product>' always fails
}
} else {
Text("Digital Good")
}
}
}
}
What I'm trying to achieve is:
I have a collection of items that depends on a class should be drawn differently
Items have different editable sync that should be binded into CartView
Not sure if thats syntax issue or my approach issue ... how to cast this on body to get the correct view based on type?
You may create a custom binding:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var product: Binding<Product>? {
guard item is Product else { return nil }
return .init(
get: {
self.$item.wrappedValue as! Product
}, set: {
self.$item.wrappedValue = $0
}
)
}
var body: some View {
VStack {
if product != nil {
TextField("add notes", text: product!.notes)
} else {
Text("Digital Good")
}
}
}
}

How to change complex data source in swiftUI List

App crash when I change data source like I tap “change data” button in APIView or delete item in QueryParametersView.list
console log:
This class 'SwiftUI.AccessibilityNode' is not a known serializable
element and returning it as an accessibility element may lead to
crashes
Fatal error: Index out of range: file
/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.8.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444
class URLComponentsModel: ObservableObject {
#Published var urlComponents = URLComponents.init()
var urlQueryItems: [URLQueryItem] {
get {
urlComponents.queryItems ?? [URLQueryItem].init()
}
set {
urlComponents.queryItems = newValue
}
}
}
struct APIView: View {
#ObservedObject var urlComponentsModel = URLComponentsModel.init()
var body: some View {
Button.init("change data") {
self.urlComponentsModel.urlComponents.queryItems?.removeFirst()
}
QueryParametersView.init(parameters: self.$urlComponentsModel.urlQueryItems)
}
}
struct QueryParametersView: View {
#Binding var parameters: [URLQueryItem]
var body: some View {
List {
ForEach(self.parameters.indices, id: \.self) { i in
HStack {
ParameterView.init(urlQueryItem: self.$parameters[i])
Text.init("delete")
.onTapGesture {
self.parameters.remove(at: i)
}
}
}
.onDelete { indices in
indices.forEach {
self.parameters.remove(at: $0)
}
}
}
}
struct ParameterView: View {
#Binding var urlQueryItem: URLQueryItem
var body: some View {
ZStack {
...
HStack {
...
if self.urlQueryItem.value != nil {
TextField("Value", text: Binding.init(get: {
(self.urlQueryItem.value ?? "")
}, set: { (value) in
self.urlQueryItem.value = value
}))
}
}
}
}
}
why? anybody help me?
removeFirst()
It says
The collection must not be empty.
If the collection is empty when you call removeFirst, app crashes with index out of range.

How to reset child view state variable with SwiftUI?

I'm sure it's something very silly but how should one reset the state value of a child view when another state has changed?
For example, the code below shows 2 folders, which respectively have 2 and 3 items., which can be edited.
If you select the second folder (Work) and its 3rd item (Peter) and then select the first folder (Home), the app crashes since selectedItemIndex is out of bounds.
I tried to "reset" the state value when the view gets initialized but it seems like changing the state like such triggers out a "runtime: SwiftUI: Modifying state during view update, this will cause undefined behavior." warning.
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
What is the proper way to do this? Thanks!
Here's the code:
AppDelegate.swift
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let store = ItemStore()
let contentView = ContentView(store: store)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
ContentView.swift
import SwiftUI
final class ItemStore: ObservableObject {
#Published var data: [Folder] = [Folder(name: "Home",
items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work",
items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
static func == (lhs: Item, rhs: Item) -> Bool {
return true
}
var id = UUID()
var name: String
var content = Date().description
init(name: String) {
self.name = name
}
}
struct ContentView: View {
#ObservedObject var store: ItemStore
#State var selectedFolderIndex: Int?
var body: some View {
HSplitView {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
Text(folder.name).tag(index)
}
}.collapsible(false)
}
.listStyle(SidebarListStyle())
// ITEMS
if selectedFolderIndex != nil {
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.frame(minWidth: 800, maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedItemIndex: Int?
var body: some View {
HSplitView {
List(selection: $selectedItemIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
}
}
.frame(width: 300)
if selectedItemIndex != nil {
DetailView(item: $items[selectedItemIndex!])
.padding()
.frame(minWidth: 200, maxHeight: .infinity)
}
}
}
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Thanks to #jordanpittman for suggesting a fix:
ItemsView(items: $store.data[selectedFolderIndex!].items).id(selectedRowIndex)
Source: https://swiftui-lab.com/swiftui-id
Fully playable sample draft for ContentView.swift. Play with it in both edit modes (inactive/active row selection) and adopt to your needs.
import SwiftUI
struct ItemStore {
var data: [Folder] = [Folder(name: "Home", items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work", items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
var id = UUID()
var name: String
var content = Date().description
}
struct ContentView: View {
#State var store: ItemStore
#State var selectedFolderIndex: Int? = 0
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
VStack {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
HStack {
Text(folder.name).tag(index)
Spacer()
}
.background(Color.white) //make the whole row tapable, not just the text
.frame(maxWidth: .infinity)
.multilineTextAlignment(.leading)
.onTapGesture {
self.selectedFolderIndex = index
}
}.onDelete(perform: delete)
}
}
.listStyle(GroupedListStyle())
.id(selectedFolderIndex)
// ITEMS
if selectedFolderIndex != nil && (($store.data.wrappedValue.startIndex..<$store.data.wrappedValue.endIndex).contains(selectedFolderIndex!) ){
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, $editMode)
}
}
func delete(at offsets: IndexSet) {
$store.wrappedValue.data.remove(atOffsets: offsets) // Note projected value! `store.data.remove() will not modify SwiftUI on changes and it will crash because of invalid index.
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedDetailIndex: Int?
var body: some View {
HStack {
List(selection: $selectedDetailIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
.onTapGesture {
self.selectedDetailIndex = index
}
}
}
if selectedDetailIndex != nil && (($items.wrappedValue.startIndex..<$items.wrappedValue.endIndex).contains(selectedDetailIndex!) ) {
DetailView(item: $items[selectedDetailIndex!])
.padding()
}
}
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(store: ItemStore())
}
}

SwiftUI: How to update passing array item in the other view

I'm trying to update arrays item with typed new value into Textfield, but List is not updated with edited value.
My Code is:
Model:
struct WalletItem: Identifiable{
let id = UUID()
var name:String
var cardNumber:String
var type:String
var cvc:String
let pin:String
var dateOfExpiry:String
}
ModelView:
class Wallet: ObservableObject{
#Published var wallets = [
WalletItem(name: "BSB", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2016-06-29"),
WalletItem(name: "Alpha bank", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2017-03-12"),
WalletItem(name: "MTБ", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2020-11-12"),
]
}
First View:
struct WalletListView: View {
// Properties
// ==========
#ObservedObject var wallet = Wallet()
#State var isNewItemSheetIsVisible = false
var body: some View {
NavigationView {
List(wallet.wallets) { walletItem in
NavigationLink(destination: EditWalletItem(walletItem: walletItem)){
Text(walletItem.name)
}
}
.navigationBarTitle("Cards", displayMode: .inline)
.navigationBarItems(
leading: Button(action: { self.isNewItemSheetIsVisible = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add item")
}
}
)
}
.sheet(isPresented: $isNewItemSheetIsVisible) {
NewWalletItem(wallet: self.wallet)
}
}
}
and Secondary View:
struct EditWalletItem: View {
#State var walletItem: WalletItem
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form{
Section(header: Text("Card Name")){
TextField("", text: $walletItem.name)
}
}
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Back")
}, trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Save")
})
}
}
P.S: If I use #Binding instead of the #State I've got an error in the first view: Initializer init(_:) requires that Binding<String> conform to StringProtocol
Here are modified parts (tested & works with Xcode 11.2 / iOS 13.2):
Sure over binding
struct EditWalletItem: View {
#Binding var walletItem: WalletItem
Place to pass it
List(Array(wallet.wallets.enumerated()), id: .element.id) { (i, walletItem) in
NavigationLink(destination: EditWalletItem(walletItem: self.$wallet.wallets[i])){
Text(walletItem.name)
}
}
ForEach(Array(list.enumerated())) will only work correctly if the list is an Array but not for an ArraySlice, and it has the downside of copying the list.
A better approach is using a .indexed() helper:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { self.base.startIndex }
var endIndex: Index { self.base.endIndex }
func index(after i: Index) -> Index {
self.base.index(after: i)
}
func index(before i: Index) -> Index {
self.base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
self.base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: self.base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Example:
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Foundation
import SwiftUI
struct Position {
var id = UUID()
var count: Int
var name: String
}
class BookingModel: ObservableObject {
#Published var positions: [Position]
init(positions: [Position] = []) {
self.positions = positions
}
}
struct EditableListExample: View {
#ObservedObject var bookingModel = BookingModel(
positions: [
Position(count: 1, name: "Candy"),
Position(count: 0, name: "Bread"),
]
)
var body: some View {
// >>> Passing a binding into an Array via index:
List(bookingModel.positions.indexed(), id: \.element.id) { i, _ in
PositionRowView(position: self.$bookingModel.positions[i])
}
}
}
struct PositionRowView: View {
#Binding var position: Position
var body: some View {
Stepper(
value: $position.count,
label: {
Text("\(position.count)x \(position.name)")
}
)
}
}
struct EditableListExample_Previews: PreviewProvider {
static var previews: some View {
EditableListExample()
}
}
See also:
How does the Apple-suggested .indexed() property work in a ForEach?

Resources