Unable to Adjust Deleted Category Totals - swiftui

I have a list of 5 expense categories that the user may create and delete. When a user category is deleted, I need to subtract the category total from the grand total and the total of all user categories. Also, any user categories following after the deleted category must have the totals shifted down one position in totalArray. In C this would be a simple operation, but in SwiftUI I don't know where to begin. Can I use the index to tell what category I'm trying to delete?
The errors include "Cannot convert value of type 'IndexSet.type' to expected argument type 'Int'", and "Expression pattern of type 'Int' cannot match values of type 'IndexSet'"
Say I have all 5 categories defined. If I delete category #2 I need to shift categories 3 - 5 totals down one position to 2 - 4 totals. When the user category totals are displayed, categories.catItem.count is used to check the validity of each category total (the first one, two, three categories, etc).
// this definition is found in different file (Class UserData)
#Published var totalArray = [Double] ()
...
struct manCatView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var body: some View {
List {
ForEach(categories.catItem) { item in
if item.noShow == false {
HStack {
Image(systemName: item.catPix).resizable()
.frame(width: 30, height: 30)
Spacer()
Text(item.catName)
}
}
}
.onDelete(perform: removeItems)
}
.navigationBarTitle(Text("Manage Categories"), displayMode: .inline)
...
func removeItems(at offsets: IndexSet) {
// at this point the deleted category still exists
// 1. must subtract 'to be deleted' category total from userTotal and grandTotal
if userData.totalArray[IndexSet] != 0 {
userData.totalArray[grandTotal] -= userData.totalArray[IndexSet]
userData.totalArray[userTotal] -= userData.totalArray[IndexSet]
}
// 2. must shift up category totals following the deleted category
switch offsets
{
case 0: // 1st category being deleted
case 1: // 2nd category being deleted
case 2: // 3rd category being deleted
case 3: // 4th category being deleted
case 4: // 5th category being deleted
default:
}
// 3. delete category
categories.catItem.remove(atOffsets: offsets)
}

I'm not sure if this is any better. I moved the category totals into the storage structure. In addition to deleting the category, the totals need to be adjusted first. When the user clicks delete, the removeItems function pointers are pointing to the category to be deleted. You would think that it would be easy to grab the category total (categories.catItem[].catTotal), but I get the error "Cannot convert value of type 'IndexSet' to expected argument type Int". I think Asperi that you want me to try some sort of mapping but I'm not sure where to begin.
struct CatItem: Codable, Identifiable {
var id = UUID()
var catNum: Int // entryView category pointer
var catName: String // category name
var catTotal: Double // category total
var catPix: String // sf symbol
var catShow: Bool // don't show certain categories
}
class Categories: ObservableObject {
#Published var catItem: [CatItem] {
}
}
...
struct manCatView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var pic: String = ""
var body: some View {
List {
ForEach(categories.catItem) { index in
if index.catShow == true {
HStack {
let pic = categories.catItem[index].catPix
Image(systemName: pic)
.resizable()
.foregroundColor(Color(colors[index]))
.frame(width: 40, height: 40)
Text("")
.frame(width: 40, alignment: .leading)
Text(index.catName)
}
}
} // end foreach
.onDelete(perform: removeItems)
} // end list
.navigationBarTitle(Text("Manage Categories"), displayMode: .inline)
}
func removeItems(at offsets: IndexSet) {
//subtract category amount from system totals.
let catTotal = categories.catItem[offsets].catTotal // <-- error here
userData.totalArray[grandTotal] -= catTotal
userData.totalArray[userTotal] -= catTotal
categories.catItem.remove(atOffsets: offsets)
}
}

Related

onChange not firing when attached to a Picker

I have a 3-part picker, and I'm trying to make the values of one Picker to be based on the value of another. Specifically adding/removing the s on the end of "Days","Weeks",etc. I have read a similar post (here) on this type of situation, but the proposed Apple solution for IOS 14+ deployments is not working. Given that the other question focuses primarily on pre-14 solutions, I thought starting a new question would be more helpful.
Can anyone shed any light on why the .onChange is never getting called? I set a breakpoint there, and it is never called when the middle wheels value change between 1 and any other value as it should.
The unconventional init is just so I could encapsulate this code removed from a larger project.
Also, I have the .id for the 3rd picker commented out in the code below, but can un-comment if the only problem remaining is for the 3rd picker to update on the change.
import SwiftUI
enum EveryType:String, Codable, CaseIterable, Identifiable {
case every="Every"
case onceIn="Once in"
var id: EveryType {self}
var description:String {
get {
return self.rawValue
}
}
}
enum EveryInterval:String, Codable, CaseIterable, Identifiable {
case days = "Day"
case weeks = "Week"
case months = "Month"
case years = "Year"
var id: EveryInterval {self}
var description:String {
get {
return self.rawValue
}
}
}
struct EventItem {
var everyType:EveryType = .onceIn
var everyInterval:EveryInterval = .days
var everyNumber:Int = Int.random(in:1...3)
}
struct ContentView: View {
init(eventItem:Binding<EventItem> = .constant(EventItem())) {
_eventItem = eventItem
}
#Binding var eventItem:EventItem
#State var intervalId:UUID = UUID()
var body: some View {
GeometryReader { geometry in
HStack {
Picker("", selection: self.$eventItem.everyType) {
ForEach(EveryType.allCases)
{ type in Text(type.description)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.3, height:100)
.compositingGroup()
.padding(0)
.clipped()
Picker("", selection: self.$eventItem.everyNumber
) {
ForEach(1..<180, id: \.self) { number in
Text(String(number)).tag(number)
}
}
//The purpase of the == 1 below is to only fire if the
// everyNumber values changes between being a 1 and
// any other value.
.onChange(of: self.eventItem.everyNumber == 1) { _ in
intervalId = UUID() //Why won't this ever happen?
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.25, height:100)
.compositingGroup()
.padding(0)
.clipped()
Picker("", selection: self.$eventItem.everyInterval) {
ForEach(EveryInterval.allCases) { interval in
Text("\(interval.description)\(self.eventItem.everyNumber == 1 ? "" : "s")")
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.4, height:100)
.compositingGroup()
.clipped()
//.id(self.intervalId)
}
}
.frame(height:100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(eventItem: .constant(EventItem()))
}
}
For Picker, its item data type must conform Identifiable and we must pass a property of item into "tag" modifier as "id" to let Picker trigger selection and return that property in Binding variable with selection.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})
//omg! I spent whole one day to find out this
Try the following
.onChange(of: self.eventItem.everyNumber) { newValue in
if newValue == 1 {
intervalId = UUID()
}
}
but it might also depend on how do you use this view, because with .constant binding nothing will change ever.
The answer by Thang Dang, above, turned out to be very helpful to me. I did not know how to conform my tag to Identifiable, but changed my tags from tag(1) to a string, as in the SwiftUI code below. The tag with a mere number in it caused nothing to happen when the Picker was set to Icosahedron (my breakpoint on setShape was never triggered), but the other three caused the correct shape to be passed in to setShape.
// set the current Shape
func setShape(value: String) {
print(value)
}
#State var shapeSelected = "Cube"
VStack {
Picker(selection: $shapeSelected, label: Text("$\(shapeSelected)")) {
Text("Cube").tag("Cube")
Text("Simplex").tag("Simplex")
Text("Pentagon (3D)").tag("Pentagon")
Text("Icosahedron").tag(1)
}.onChange(of: shapeSelected, perform: { tag in
setShape(value: "\(tag)")
})
}

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 Nested ForEach causes Fatal error: each layout item may only occur once

I'm trying to create a LazyVGrid view to display the contents of objects in an array using nested ForEach statements. The code is causing an app crash with the message "Fatal error: each layout item may only occur once".
Each object contains an array of values that need to be displayed as a row in the grid. The row consists of cell with the object's sku string followed by a number of cells with the integers from the object's array.
I chose to use a class instead of a struct because I need to update the array within the class.
This is the class for the objects in the array.
class RoomPickupData: Identifiable {
let id = UUID()
var sku: String = ""
// Array of counts for sku on a particular date
var roomsForDate: [Date: Int] = [:]
}
In the code the first ForEach is used to put header information in the first line of the grid.
The next ForEach and the ForEach nested in it are causing the error.
The outer ForEach iterates through the array of objects so I can create a row in the grid for each object. Each row consists of a string followed values from the roomsForDate array. The size of the roomsForDate array is not fixed.
If I comment out the nested ForEach the code executes but with it I get the fatal error.
If I comment out Text(roomData.value.description) so there is noting in the inner ForEach, the code runs fine too. If I replace Text(roomData.value.description) with Text("") the app crashes.
ForEach nesting seems like the best way to accomplish producing a grid view from a two dimensional array or array of objects each containing an array.
I've found other posts showing that nested ForEach statements can be used in SwiftUI.
Here is the code. Your help with this issue is appreciated.
struct RoomTableView: View {
#ObservedObject var checkfrontVM: CheckFrontVM
let dateFormatter = DateFormatter()
init(checkfrontVM: CheckFrontVM) {
self.checkfrontVM = checkfrontVM
dateFormatter.dateFormat = "MM/dd/yyyy"
}
func initColumns() -> [GridItem] {
var columns: [GridItem]
var columnCount = 0
if self.checkfrontVM.roomPickupDataArray.first != nil {
columnCount = self.checkfrontVM.roomPickupDataArray.first!.roomsForDate.count
} else {
return []
}
columns = [GridItem(.flexible(minimum: 100))] + Array(repeating: .init(.flexible(minimum: 100)), count: columnCount)
return columns
}
var body: some View {
if self.checkfrontVM.roomPickupDataArray.first != nil {
ScrollView([.horizontal, .vertical]) {
LazyVGrid(columns: initColumns()) {
Text("Blank")
ForEach(self.checkfrontVM.roomPickupDataArray.first!.roomsForDate.sorted(by: {
$0 < $1
}), id: \.key) { roomData in
Text(dateFormatter.string(from: roomData.key))
}
ForEach(self.checkfrontVM.roomPickupDataArray) { roomPickupData in
Group {
Text(roomPickupData.sku)
.frame(width: 80, alignment: .leading)
.background(roomTypeColor[roomPickupData.sku])
.border(/*#START_MENU_TOKEN#*/Color.black/*#END_MENU_TOKEN#*/)
ForEach(roomPickupData.roomsForDate.sorted(by: { $0.key < $1.key}), id: \.key) { roomData in
Text(roomData.value.description)
}
}
}
}
.frame(height: 600, alignment: .topLeading)
}
.padding(.all, 10)
}
}
}
Every item in ForEach needs a unique ID. The code can be modified by adding the following line to the Text view in the inner ForEach to assign a unique ID:
.id("body\(roomPickupData.id)-\(roomData.key)")
ForEach(self.checkfrontVM.roomPickupDataArray) { roomPickupData in
Group {
Text(roomPickupData.sku)
.frame(width: 80, alignment: .leading)
.background(roomTypeColor[roomPickupData.sku])
.border(Color.black)
ForEach(roomPickupData.roomsForDate.sorted(by: { $0.key < $1.key}), id: \.key) { roomData in
Text(roomData.value.description)
.id("body\(roomPickupData.id)-\(roomData.key)") //<-
}
}
}
I was facing the same error for ForEach inside ForEach inside a LazyVStack I have solved it by adding the UUID() identifier to the innermost View like
.id(UUID)
Here is an example:
LazyVStack {
ForEach(vmHome.allCategories, id: \.self) { category in
VStack {
HStack {
Text(category)
.font(.title3)
.bold()
.padding(10)
Spacer()
}
ScrollView(.horizontal) {
LazyHStack{
ForEach(vmHome.getMovies(forCat: category), id: \.self.id) { movie in
StandardHomeMovieView(movie: movie)
.id(UUID())
}
}
}
}
}
}

SwiftUI How to Have a ForEach loop iterate based on how many days of a selected month in a Picker is

So I created a line of code that takes two values, a year and a month, and depending on what values you give it, it will calculate the number of days in a given month for a given year. Initially I was going to use State variables so it would just update automatically, but because of how Structs work, I couldn't use the variables I had just barely initialized. (as in the "Cannot use instance member 'year' within property initializer; property initializers run before 'self' is available" Error). The reason I want this code is because I want a forEach Loop to automatically iterate based on that number (as the app I am making will have a list for every day for the next two years). Here is my code:
struct YearView: View {
#State var year = [2020, 2021, 2022]
//monthSymbols gets an array of all the months
#State var monthArray = DateFormatter().monthSymbols!
#State var yearIndex = 0
#State var monthIndex = 0
#State var month = 0
#State var daysInMonth = Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
var body: some View {
NavigationView {
List {
Section {
VStack {
Picker("Years", selection: $yearIndex) {
ForEach(0 ..< year.count) { index in
Text(String(self.year[index])).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
if yearIndex == 0 {
Picker("Month", selection: $monthIndex) {
ForEach(6 ..< monthArray.count) { index in
Text(self.monthArray[index]).tag(index)
}
}
.padding(.bottom, 2)
} else {
Picker("Month", selection: $monthIndex) {
ForEach(0 ..< monthArray.count) { index in
Text(self.monthArray[index]).tag(index)
}
}
.padding(.bottom, 2)
}
}
}
Section {
ForEach(0..<10) { i in
NavigationLink(destination: ContentView(day: dayData[i])) {
DayRow(day: dayData[i])
}
}
}
}
.navigationBarTitle(Text("\(monthArray[monthIndex + indexTest]) \(String(year[yearIndex]))"))
}
.listStyle(InsetGroupedListStyle())
}
}
The ForEach loop that is by the Navigation Link is the one I want to be iterated. I have tried creating a function as such:
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
}
I'm not sure where I would run that however, if that would even work. I'm new to SwiftUI and GitHub, so if there's any more info I can give that would help, just ask!
As far as I can tell you should be able to call your getRange(year:, month:) function in your ForEach. Passing in yearIndex and monthIndex.
I wrote up this little sample code to quickly test the theory. Let me know if this is what you're looking for.
-Dan
struct ContentView: View {
#State private var year = 3
#State private var month = 2
var body: some View {
List {
ForEach(0 ..< getRange(year: year, month: month)) { i in
Text("\(i)")
}
}
}
func getRange(year: Int, month: Int) -> Int {
return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month + 1))!)!.count
}
}
edit to address the question in your comment;
To update your Picker label properly try changing your code to this:
Picker("Years", selection: $year) {
ForEach(0 ..< year) { index in
Text("\(index)")
}
}
See if that gives you the desired response. It's not what you're doing is entirely wrong, I'm just not super clear on what your ultimate intent is. But play around with the code, I think you're on the right track.

Make Equal-Width SwiftUI Views in List Rows

I have a List that displays days in the current month. Each row in the List contains the abbreviated day, a Divider, and the day number within a VStack. The VStack is then embedded in an HStack so that I can have more text to the right of the day and number.
struct DayListItem : View {
// MARK: - Properties
let date: Date
private let weekdayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return formatter
}()
private let dayNumberFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "d"
return formatter
}()
var body: some View {
HStack {
VStack(alignment: .center) {
Text(weekdayFormatter.string(from: date))
.font(.caption)
.foregroundColor(.secondary)
Text(dayNumberFormatter.string(from: date))
.font(.body)
.foregroundColor(.red)
}
Divider()
}
}
}
Instances of DayListItem are used in ContentView:
struct ContentView : View {
// MARK: - Properties
private let dataProvider = CalendricalDataProvider()
private var navigationBarTitle: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM YYY"
return formatter.string(from: Date())
}
private var currentMonth: Month {
dataProvider.currentMonth
}
private var months: [Month] {
return dataProvider.monthsInRelativeYear
}
var body: some View {
NavigationView {
List(currentMonth.days.identified(by: \.self)) { date in
DayListItem(date: date)
}
.navigationBarTitle(Text(navigationBarTitle))
.listStyle(.grouped)
}
}
}
The result of the code is below:
It may not be obvious, but the dividers are not lined up because the width of the text can vary from row to row. What I would like to achieve is to have the views that contains the day information be the same width so that they are visually aligned.
I have tried using a GeometryReader and the frame() modifiers to set the minimum width, ideal width, and maximum width, but I need to ensure that the text can shrink and grow with Dynamic Type settings; I chose not to use a width that is a percentage of the parent because I was uncertain how to be sure that localized text would always fit within the allowed width.
How can I modify my views so that each view in the row is the same width, regardless of the width of text?
Regarding Dynamic Type, I will create a different layout to be used when that setting is changed.
I got this to work using GeometryReader and Preferences.
First, in ContentView, add this property:
#State var maxLabelWidth: CGFloat = .zero
Then, in DayListItem, add this property:
#Binding var maxLabelWidth: CGFloat
Next, in ContentView, pass self.$maxLabelWidth to each instance of DayListItem:
List(currentMonth.days.identified(by: \.self)) { date in
DayListItem(date: date, maxLabelWidth: self.$maxLabelWidth)
}
Now, create a struct called MaxWidthPreferenceKey:
struct MaxWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let nextValue = nextValue()
guard nextValue > value else { return }
value = nextValue
}
}
This conforms to the PreferenceKey protocol, allowing you to use this struct as a key when communicating preferences between your views.
Next, create a View called DayListItemGeometry - this will be used to determine the width of the VStack in DayListItem:
struct DayListItemGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width)
}
.scaledToFill()
}
}
Then, in DayListItem, change your code to this:
HStack {
VStack(alignment: .center) {
Text(weekdayFormatter.string(from: date))
.font(.caption)
.foregroundColor(.secondary)
Text(dayNumberFormatter.string(from: date))
.font(.body)
.foregroundColor(.red)
}
.background(DayListItemGeometry())
.onPreferenceChange(MaxWidthPreferenceKey.self) {
self.maxLabelWidth = $0
}
.frame(width: self.maxLabelWidth)
Divider()
}
What I've done is I've created a GeometryReader and applied it to the background of the VStack. The geometry tells me the dimensions of the VStack which sizes itself according to the size of the text. MaxWidthPreferenceKey gets updated whenever the geometry changes, and after the reduce function inside MaxWidthPreferenceKey calculates the maximum width, I read the preference change and update self.maxLabelWidth. I then set the frame of the VStack to be .frame(width: self.maxLabelWidth), and since maxLabelWidth is binding, every DayListItem is updated when a new maxLabelWidth is calculated. Keep in mind that the order matters here. Placing the .frame modifier before .background and .onPreferenceChange will not work.
I was trying to achieve something similar. My text in one of the label in a row was varying from 2 characters to 20 characters. It messes up the horizontal alignment. I was looking to make this column in row as fixed width. And here is a very simple solution I applied to achieve that and it worked for me. Hope it can benefit someone else too.
var body: some View { // view for each row in list
VStack(){
HStack {
Text(wire.labelValueDate)
.
.
.foregroundColor(wire.labelColor)
.fixedSize(horizontal: true, vertical: false)
.frame(width: 110.0, alignment: .trailing)
}
}
}