Swift UI, removing item from array, while looping in it throws Fatal Error: Index out of range [duplicate] - struct

I am trying to remove rows inside a ForEach. Removing the last row always throws an index out of range exception. Removing any other row does not.
ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
HStack {
if self.isEditSelected {
Button(action: {
self.player.scores.remove(at: index)
}, label: {
Image("delete")
})
}
TextField("\(score)", value: self.$player.scores[index], formatter: NumberFormatter())
}
}
I have tried using ForEach(player.indices...) & ForEach(player.scores...), but see the same problem.
Looks to me like the crash happens here self.$player.scores[index], as hardcoding the index to any value other that the last row is working.
Does anyone know how to fix this? Or if there is a better approach.

Here is fix
ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
HStack {
if self.isEditSelected {
Button(action: {
self.player.scores.remove(at: index)
}, label: {
Image("delete")
})
}
TextField("\(score)", value: Binding( // << use proxy binding !!
get: { self.player.scores[index] },
set: { self.player.scores[index] = $0 }),
formatter: NumberFormatter())
}
}

Based on #Asperi answer
public extension Binding where Value: Equatable {
static func proxy(_ source: Binding<Value>) -> Binding<Value> {
self.init(
get: { source.wrappedValue },
set: { source.wrappedValue = $0 }
)
}
}
You can use this as follows:
TextField("Name", text: .proxy($variable))

Xcode 13.0 beta introduced a new way to establish two-way-bindings between the elements of a collection and the views built by ForEach / List.
This method fixes the crash related to deleting the last row.
struct Score: Identifiable {
let id = UUID()
var value: Int
}
struct Player {
var scores: [Score] = (1...10).map {_ in .init(value: Int.random(in: 0...25))}
}
struct BindingTest: View {
#State private var player = Player()
var body: some View {
List {
ForEach($player.scores) { $score in
HStack {
TextField("\(score.value)", value: $score.value,
formatter: NumberFormatter())
}
}
.onDelete { player.scores.remove(atOffsets: $0)}
}
}
}

Related

SwiftUI UISearchController replacement: search field, results and some scrollable content fail to coexist in a meaningful manner

Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!
If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples 🍏",
"Cherries πŸ’",
"Pears 🍐",
"Oranges 🍊",
"Pineapples 🍍",
"Bananas 🍌"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}

SwiftUI: How to show/edit an int from CoreData without being in a List?

self-learning beginner here. I am trying to show an Int from Core Data in a VStack in ContentView, not in a List. But literally all the tutorials I can find about Core Data (tracking Books, Movies, Orders, Students) are using a List to show an array containing an Int. Nothing on showing an Int by itself.
Xcode can build countnum.countnum +=1 with no problem. Seems to me it is reading it fine. But once I try to show it, it just doesn’t work. I’m wrecking my brain here.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
// let countnum = CountNum(context: moc)
VStack{
Text("+")
.padding()
.onTapGesture (count: 2){
let countnum = CountNum(context: moc)
countnum.countnum += 1
}
Text("\(countnum)") //No exact matches in call to instance method 'appendInterpolation'
}
}
}
Thanks
....all the tutorials ... show an array containing an Int. Yes, that's because CoreData
can contain many "objects". You get an array of your CountNum objects when
you do your .....var countnum: FetchedResults<CountNum>. So you need to decide which CountNum you want to
use. For example, if you want to use the first one, then:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
VStack {
if let firstItem = countnum.first {
Text("+")
.padding()
.onTapGesture(count: 2) {
firstItem.countnum += 1
do {
try moc.save()
} catch {
print(error)
}
}
Text("\(firstItem.countnum)").foregroundColor(.green)
}
}
}
}
EDIT-1: adding new CountNum to CoreData example code in the add button.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
Button(action: {add()}) { Text("add new CountNum").foregroundColor(.green) }
.padding(.top, 50)
List {
ForEach(countnum) { item in
HStack {
Text("++")
.onTapGesture(count: 2) { increment(item) }
Text("\(item.countnum)").foregroundColor(.blue)
Text("delete").foregroundColor(.red)
.onTapGesture { delete(item: item) }
}
}
}
}
func increment(_ item: CountNum) {
item.countnum += 1
save()
}
func add() {
let countnum = CountNum(context: moc)
countnum.countnum = 0
save()
}
func delete(item: CountNum) {
moc.delete(item)
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}

Deleting CoreData without onDelete in SwiftUI

my app shows CoreData as following in VStack. List isn't possible due to ScrollView.
VStack (spacing: 20) {
ForEach(groups) { group in
NavigationLink(destination: GroupView()) {
ZStack (alignment: .bottomLeading) {
Image(uiImage: (UIImage(data: group.groupThumbnail ?? self.image) ?? UIImage(named: "defaultGroupThumbnail"))!)
.resizable(capInsets: EdgeInsets())
.aspectRatio(contentMode: .fill)
.frame(height: 200, alignment: .center)
.cornerRadius(22)
VStack (alignment: .leading) {
Text("\(group.groupTitle ?? "Untitled")")
.font(.title)
.fontWeight(.heavy)
.multilineTextAlignment(.leading)
Text("Consists of 5 Flowers")
}
.padding([.leading, .bottom], 18.0)
.foregroundColor(.primary)
}
However, I can delete my entries with onDelete. Therefore, I am trying to find an alternative with .contextMenu.
.contextMenu {
Button (role: .destructive) {
withAnimation {
self.deleteGroups(at: IndexSet.init(arrayLiteral: 0)) // This is the code I copied from the video.
}
} label: {
Label("Delete", systemImage: "trash")
}
Button {
print()
} label: {
Label("Edit", systemImage: "square.and.pencil")
}
}
I saw a video in which somebody was able to delete his entry with the combination of these to codes
When tapping on "Delete", the entry should be deleted by this code:
func deleteGroups(at offsets: IndexSet) {
for offset in offsets {
let group = groups[offset]
viewContext.delete(group)
}
//try? viewContext.save()
}
But, whenever I click this "Delete" Button in context menu, the wrong one gets deleted. I can't get the code to delete the selected entry. If anyone can help me, that would be appreciated. Thanks in Advance!
Not that important but, is there a delete feature compared to "onDelete" (such as swiping to delete) I can still implement? And are there any possibilities for animation in my case?
Kind Regards
Just delete it in the normal way:
Button (role: .destructive) {
withAnimation {
viewContext.delete(group)
do {
try viewContext.save()
} catch {
// show error
}
}
} label: {
Label("Delete", systemImage: "trash")
}
FYI I also edited your question to fix your ForEach syntax.
ForEach(groups, id: \.self) { group in
.contextMenu {
Button (role: .destructive) {
for index in groups.indices{
if group == groups[index]{
withAnimation{
viewContext.delete(group)
}
}
}
} label: {
Label("Delete", systemImage: "trash")
}
Button {
print()
} label: {
Label("Edit", systemImage: "square.and.pencil")
}
}

Why is my NSFetchRequest not updating my array as I expect? And does `shouldRefreshRefetchedObjects` make any difference?

I do not understand why this code does not work to update a list when navigating "back" from a DetailView(). As far as I can tell, I'm calling a new fetchRequest each time I want to update the list and it seems that request should always return object with current properties. But as others have said they are "stale", reflecting whatever was the property BEFORE the update was committed in the DetailView. And tapping a Navigation link from a "Stale" row, opens a DetailView with the current values of the properties, so I know they have been sacved to the context (haven't they?).
First I have a "dataservice" like this:
import CoreData
import SwiftUI
protocol CategoryDataServiceProtocol {
func getCategories() -> [Category]
func getCategoryById(id: NSManagedObjectID) -> Category?
func addCategory(name: String, color: String)
func updateCategory(_ category: Category)
func deleteCategory(_ category: Category)
}
class CategoryDataService: CategoryDataServiceProtocol {
var viewContext: NSManagedObjectContext = PersistenceController.shared.viewContext
///Shouldn't this next function always return an updated version of my list of categories?
func getCategories() -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
let sort: NSSortDescriptor = NSSortDescriptor(keyPath: \Category.name_, ascending: true)
request.sortDescriptors = [sort]
///This line appears to do nothing if I insert it:
request.shouldRefreshRefetchedObjects = true
do {
///A print statement here does run, so it's getting this far...
print("Inside get categories func")
return try viewContext.fetch(request)
} catch {
return []
}
}
func getCategoryById(id: NSManagedObjectID) -> Category? {
do {
return try viewContext.existingObject(with: id) as? Category
} catch {
return nil
}
}
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
func updateCategory(_ category: Category) {
saveContext()
}
func deleteCategory(_ category: Category) {
viewContext.delete(category)
saveContext()
}
func saveContext() {
PersistenceController.shared.save()
}
}
class MockCategoryDataService: CategoryDataService {
override init() {
super .init()
self.viewContext = PersistenceController.preview.viewContext
print("MOCK INIT")
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
}
}
And I have a viewModel like this:
import SwiftUI
extension CategoriesList {
class ViewModel: ObservableObject {
let dataService: CategoryDataServiceProtocol
#Published var categories: [Category] = []
init(dataService: CategoryDataServiceProtocol = CategoryDataService()) {
self.dataService = dataService
}
func getCategories() {
self.categories = dataService.getCategories()
}
func deleteCategories(at offsets: IndexSet) {
offsets.forEach { index in
let category = categories[index]
dataService.deleteCategory(category)
}
}
}
}
Then my view:
import SwiftUI
struct CategoriesList: View {
#StateObject private var viewModel: CategoriesList.ViewModel
init(viewModel: CategoriesList.ViewModel = .init()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
#State private var isShowingSheet = false
var body: some View {
NavigationView {
List {
ForEach(viewModel.categories) { category in
NavigationLink(
destination: CategoryDetail(category: category)) {
CategoryRow(category: category)
.padding(0)
}
}
.onDelete(perform: { index in
viewModel.deleteCategories(at: index)
viewModel.getCategories()
})
}
.listStyle(PlainListStyle())
.onAppear(perform: {
viewModel.getCategories()
})
.navigationBarTitle(Text("Categories"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: { EditButton() })
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: {
isShowingSheet = true
viewModel.getCategories()
},
label: { Image(systemName: "plus.circle").font(.system(size: 20)) }
)
}
}
.sheet(isPresented: $isShowingSheet, onDismiss: {
viewModel.getCategories()
}, content: {
CategoryForm()
})
}
}
}
struct CategoriesList_Previews: PreviewProvider {
static var previews: some View {
let viewModel: CategoriesList.ViewModel = .init(dataService: MockCategoryDataService())
return CategoriesList(viewModel: viewModel)
}
}
So, when I navigate to the DetailView and change the name of the category, all is fine there. But then tapping the back button or swiping to return to the view - and the view still shows the old name.
I understand that the #Published array of [Category] is probably not looking at changes to objects inside the array, only if an object is removed or added, I guess.
But why is my list not updating anyways, since I am calling viewModel.getCategories() and that is triggering the fetch request in the dataservice getCategories function?
And if Combine is the answer, then how? Or what else am I missing? Does request.shouldRefreshRefetchedObjects = true offer anything? Or is it a bug as I read here: https://mjtsai.com/blog/2019/10/17/core-data-derived-attributes/

Cannot convert value of type 'NSSet?' to expected argument type 'Range<Int>' (using CoreData)

Using CoreData I don't know how to solve that problem:
Error: Cannot convert value of type 'NSSet?' to expected argument type 'Range<Int>'
private func displayTopics(subject: Subject) -> some View {
NavigationView {
Form {
List {
ForEach(subject.topics) { topic in
NavigationLink(
destination: Text("Destination"),
label: {
Text("Navigate")
})
}
}
}
}
What should I do?
ForEach does not understand NSSet, you have to convert it in array:
if subject.topics != nil {
ForEach(Array(subject.topics! as Set), id: \.self) { topic in
NavigationLink(
destination: Text("Destination"),
label: {
Text("Navigate")
})
}
}

Resources