SwiftUI delete and move functionality - swift

I'm running into some issues with my move and delete methods. This is a follow up to this question: SwiftUI Section from attribute of a struct
I'm trying to group people by company, and the solution provided in the previous question works great. It does have an effect on my move and delete methods and I'm finding it difficult to figure out why.
The delete function appears to be deleting rows that I didn't select, and the move method crashes with Attempt to create two animations for cell.
struct Person: Identifiable {
var id = UUID()
var name: String
var company: String
}
class PeopleList: ObservableObject {
#Published var people = [
Person(name: "Bob", company: "Apple"),
Person(name: "Bill", company: "Microsoft"),
Person(name: "Brenda", company: "Apple"),
Person(name: "Lucas", company: "Microsoft"),
]
func getGroups() -> [String] {
var groups : [String] = []
for person in people {
if !groups.contains(person.company) {
groups.append(person.company)
}
}
return groups
}
func deleteListItem(whichElement: IndexSet) {
people.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, destination: Int) {
people.move(fromOffsets: whichElement, toOffset: destination)
}
}
struct ContentView: View {
#ObservedObject var peopleList = PeopleList()
var body: some View {
NavigationView {
List () {
ForEach (peopleList.getGroups(), id: \.self) { group in
Section(header: Text(group)) {
ForEach(self.peopleList.people.filter { $0.company == group }) { person in
Text(person.name)
}
.onDelete(perform: self.peopleList.deleteListItem)
.onMove(perform: self.peopleList.moveListItem)
}
}
}
.listStyle(GroupedListStyle())
.navigationBarItems(trailing: EditButton())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

UPDATED ANSWER - now with new datamodel and working deletion
try this:
struct Person: Identifiable, Hashable {
var id = UUID()
var name: String
}
struct Company : Identifiable, Hashable {
var id = UUID()
var name: String
var employees : [Person]
}
class CompanyList: ObservableObject {
#Published var companies = [
Company(name: "Apple", employees: [Person(name:"Bob"), Person(name:"Brenda")]),
Company(name: "Microsoft", employees: [Person(name:"Bill"), Person(name:"Lucas")])
]
func deleteListItem(whichElement: IndexSet, from company: Company) {
let index = companies.firstIndex(of: company)!
companies[index].employees.remove(atOffsets: whichElement)
}
// func moveListItem(whichElement: IndexSet, destination: Int) {
// companies.employees.move(fromOffsets: whichElement, toOffset: destination)
// }
}
struct ContentView: View {
#ObservedObject var companyList = CompanyList()
#State var text : String = ""
var body: some View {
NavigationView {
VStack {
List () {
ForEach (companyList.companies, id: \.self) { company in
Section(header: Text(company.name)) {
ForEach(company.employees) { employee in
Text(employee.name).id(UUID())
}
.onDelete { (indexSet) in
self.text = ("\(indexSet), \(indexSet.first)")
self.companyList.deleteListItem(whichElement: indexSet, from: company)
}
// .onMove(perform: self.companyList.moveListItem)
}
}
}
.listStyle(GroupedListStyle())
.navigationBarItems(trailing: EditButton())
Text(text)
}
}
}
}

Related

SwiftUI: Missing argument for parameter 'x' in call

My content view has a StateObject to a data model "Pilot." The complier generates an error of "Missing argument for parameter 'pilot' in call. Xcode offers the following fix: var pilot = Pilot(pilot: [Employee])... however, the complier generates a new error on that fix: "Cannot convert value of type '[Employee].Type' to expected argument type '[Employee]'"
Here is my Content View:
struct ContentView: View {
#StateObject var pilot = Pilot(pilot: [Employee])
var body: some View {
NavigationView {
ZStack {
Color.gray.ignoresSafeArea()
.navigationBarHidden(true)
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
EmployeeView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.padding()
}
.environmentObject(pilot)
}
}
}
}
Here is my data model:
class Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct Data {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var data: Data {
Data(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
}
class Data: Employee {
static let sampleData: [Employee] = [
Employee(age: 35, yearGroup: 1, category: "B717")
]
}
I am also getting a similar compiler error in my Content view for the "Employee View()" which states: "Missing argument for parameter 'data' in call"
Here is my EmployeeView code:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let data: [Employee]
var body: some View {
ZStack {
List {
ForEach(data) {line in
EmployeeCardView(employee: line)
}
}
}
}
}
/// UPDATE1 ///
I tried to pass an instance of Employee to Pilot but I've hit a new wall. Here is my new code.
Here is my data model:
struct Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct UserInfo {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var userInfo: UserInfo {
UserInfo(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
let pilotInfo = Employee(age: 35, yearGroup: 1, category: "B717")
}
And here is my Content view:
struct ContentView: View {
#StateObject var pilot = Pilot(pilotInfo) //<error here>
Now getting an error in the Content view: "Cannot find 'pilotInfo' in scope"
/// UPDATE 2 ///
I removed the UserInfo section of the model data and followed the guidance to take one Employee (not an array) and change the Content view variable. That fixed those associated errors.
In an effort to comply with principles outlined in developer.apple.com/tutorials/app-dev-training/displaying-data-in-a-list, I've tried to match the following from Apple's tutorial:
struct ScrumsView: View {
let scrums: [DailyScrum]
var body: some View {
List {
ForEach(scrums, id: \.title) { scrum in
CardView(scrum: scrum)
.listRowBackground(scrum.theme.mainColor)
}
}
}
}
That's why my Employee view looks like this:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let userInfo: [Pilot]
var body: some View {
ZStack {
List {
ForEach(userInfo, id: \.age) {line in //ERRORs
EmployeeCardView(employee: line)
}
}
}
}
}
The complier errors are:
Cannot convert value of type '[Pilot]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
Key path value type '' cannot be converted to contextual type ''
///UPDATE 3///
struct ProfileFormView: View {
#EnvironmentObject var pilot: Pilot
#StateObject var vm: EmployeeViewModel = EmployeeViewModel()
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {
DatePicker("Birthdate", selection: $vm.birthdate, displayedComponents: .date)
DatePicker("New Hire Date", selection: $vm.newHireDate, displayedComponents: .date)
Picker("Your Current Aircraft", selection: $vm.chosenAircraft) {
ForEach(vm.currentAircraft, id: \.self) {
Text ($0)
}
}
}
}
}
}
}
As you can see, my first attempt was much more complex but since I could not get my previous version views to take user input updates, I decided to start over with a more basic app to better learn the fundamentals of data management.

swiftUI: updating view

I can't figure it out why view is not updating, please help. In real project I get data via websocket (and set variable with DispatchQueue.main.async {}). Here's the code as an example. After clicking on button nothing happens with the view. I use ObservableObject, Published attributes. What's the problem?
ps. It requires to add some more text to the post, because it's mostly the code, but I don't know what to add, everything is below :)
import SwiftUI
class DataBase: ObservableObject {
#Published var data: [MyData]
#Published var users: [User]
init(data: [MyData], users: [User]) {
self.data = data
self.users = users
}
}
class MyData: ObservableObject, Identifiable {
#Published var type: String
#Published var array: [Double]
init(type: String, array: [Double]) {
self.type = type
self.array = array
}
}
class User: ObservableObject, Identifiable {
#Published var id: UUID = UUID()
#Published var name: String
#Published var data: MyData
init(name: String, data: MyData) {
self.name = name
self.data = data
}
}
let data: [MyData] = [
MyData(type: "type1", array: [1, 2, 3]),
MyData(type: "type2", array: [4, 5, 6, 7]),
]
let users: [User] = [
User(name: "Tim", data: data[0]),
User(name: "Steve", data: data[1]),
]
struct ContentView: View {
let db = DataBase(data: data, users: users)
var body: some View {
ShowView(db: db)
}
}
struct ShowView: View {
#ObservedObject var db: DataBase
var body: some View {
HStack {
List(db.users) { user in
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
List(db.data) { data in
Text("\(data.type)")
Text("\(data.array.count)")
Divider()
}
}
HStack {
Button("add data to data[0]") {
db.data[0].array.append(db.data[0].array.last! + 10)
print(db.data[0].array)
}
Button("add data to data[1]") {
db.data[1].array.append(db.data[1].array.last! + 20)
print(db.data[1].array)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
try this using objectWillChange, works for me:
struct ShowView: View {
#ObservedObject var db: DataBase
var body: some View {
HStack {
List(db.users) { user in
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
List(db.data) { data in
Text("\(data.type)")
Text("\(data.array.count)")
Divider()
}
}
HStack {
Button("add data to data[0]") {
db.objectWillChange.send() // <-- here
db.data[0].array.append(db.data[0].array.last! + 10)
print(db.data[0].array)
}
Button("add data to data[1]") {
db.objectWillChange.send() // <-- here
db.data[1].array.append(db.data[1].array.last! + 20)
print(db.data[1].array)
}
}
}
}
Just make model as value type (i.e. struct instead of class) - no more changes needed:
struct MyData: Identifiable {
var id = UUID()
var type: String
var array: [Double]
init(type: String, array: [Double]) {
self.type = type
self.array = array
}
}
struct User: Identifiable {
var id: UUID = UUID()
var name: String
var data: MyData
init(name: String, data: MyData) {
self.name = name
self.data = data
}
}
Tested with Xcode 13.4 / iOS 15.5
Update
Then it is needed to create separated views with ObservedObject for every observable model object, like
List(db.users) {
UserRowView(user: $0)
}
struct UserRowView: View {
#ObservedObject var user: User // a class, so needed to be observed
var body: some View {
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
}
the same for MyData, or make a dependency update, like
class User: ObservableObject {
#Published var data: MyData
// ...
private var cancellable: AnyCancellable?
init(...) {
// ....
cancellable = data.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.objectWillChange.send()
}
}
}

How do i change a StateObject Array in a forEach?

I have a StateObject that is being initialized, it contains an Array of "Activity" which itself contains a property name that holds a string. When i iterate the array in a forEach i try to change the name of the Activity in another view yet the change doesn't show in the List. Here is the code:
struct ContentView: View {
#StateObject var store = ActivityStore()
#State var showAdd = false
var body: some View {
NavigationView {
VStack {
if store.Activities.isEmpty {
Text("Please Add an Activity")
} else {
List {
Section(header: Text("Activities: ") ) {
ForEach(store.Activities) { activity in
NavigationLink( destination: DetailView(activity: activity))
{
Text(verbatim: activity.name)
}
}
}
}
}
}
.navigationTitle("ITrack")
.navigationBarItems(trailing: Button(action: { showAdd = true }) {
Text("Add")
})
.sheet(isPresented: $showAdd) {
AddView(store: store)
}
}
}
}
struct DetailView: View {
#Binding var activity: Activity
var body: some View {
VStack {
TextField("name", text: $activity.name )
Text(activity.description)
}
}
}
struct Activity: Identifiable {
var id = UUID()
var name: String
var description: String
var log: [String] = []
init(name: String, description: String) {
self.name = name
self.description = description
}
}
class ActivityStore: ObservableObject {
#Published var Activities: [Activity] = []
func demo() -> [Activity] {
let activities: [Activity] = []
Activities.append(Activity(name: "br1", description: "br111111"))
Activities.append(Activity(name: "br2", description: "br222222"))
Activities.append(Activity(name: "br3", description: "br333333"))
Activities.append(Activity(name: "br4", description: "br444444"))
return activities
}
}
Here are needed updates
1)
struct Activity: Identifiable, Hashable {
ForEach(Array(store.Activities.enumerated()), id: \.1) { i, activity in
NavigationLink( destination: DetailView(activity: $store.Activities[i]))
{
Text(verbatim: activity.name)
}
}

SwiftUI Section from attribute of a struct

Let's say I've got
struct Person: Identifiable {
var id = UUID()
var name: String
var company: String
}
I also have an array of people, like so:
class PeopleList: ObservableObject {
#Published var people = [
Person(name: "Bob", company: "Apple"),
Person(name: "Bill", company: "Microsoft"),
Person(name: "Brenda", company: "Apple"),
Person(name: "Lucas", company: "Microsoft"),
]
//Various delete and move methods
}
I'd now like to create a list with sections, where every person is grouped based on their company. I've gotten to the following, but this gives me grouped sections for each person, so 4 sections. I'd like to end up with 2 sections, one for Apple and one for Microsoft.
struct PeopleView: View {
#ObservedObject var peopleList = PeopleList()
var body: some View {
NavigationView {
List {
ForEach(peopleList.people) { person in
Section(header: Text(person.company)) {
Text(person.name)
}
}
}
.listStyle(GroupedListStyle())
}
}
}
I hope that makes sense! Thanks!
try this:
struct Person: Identifiable {
var id = UUID()
var name: String
var company: String
}
class PeopleList: ObservableObject {
#Published var people = [
Person(name: "Bob", company: "Apple"),
Person(name: "Bill", company: "Microsoft"),
Person(name: "Brenda", company: "Apple"),
Person(name: "Lucas", company: "Microsoft"),
]
func getGroups() -> [String] {
var groups : [String] = []
for person in people {
if !groups.contains(person.company) {
groups.append(person.company)
}
}
return groups
}
}
struct ContentView: View {
#ObservedObject var peopleList = PeopleList()
var body: some View {
NavigationView {
List () {
ForEach (peopleList.getGroups(), id: \.self) { group in
Section(header: Text(group)) {
ForEach(self.peopleList.people.filter { $0.company == group }) { person in
Text(person.name)
}
}
}
}.listStyle(GroupedListStyle())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Difficulty Updating String With a TextField in SwiftUI

I am currently having trouble modifying a String value using a TextField. Here is my (simplified) code so far:
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
}
var courses: [Course] {
// initialization code...
}
struct GradeCalculatorView: View {
#State var selectedCourseIndex: Int = 0
var body: some View {
VStack {
// allows user to select a course:
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
}, label: {
Text(courses[i].name)
})
}
CourseView(course: courses[selectedCourseIndex])
}
}
}
struct CourseView: View {
#ObservedObject var course: Course // passed in from GradeCalculatorView
var body: some View {
VStack(alignment: .leading) {
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
}.padding(.leading).frame(alignment: .leading)
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: HorizontalAlignment.leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id:\.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name)
}
}
I cannot seem to modify the GradeItem object's name using the TextField. When the TextField is edited, its text changes temporarily. However, when the GradeItemRow View is reloaded, it displays the GradeItem object's original name, rather than its updated name.
Would somebody please be able to help?
Thanks in advance
UPDATE: As per your requests, I have added more context to this sample code.
I know that this does not work, as when I attempt to modify a GradeItem's name with a TextField, it changes temporarily. However, when I select a different course and then the course I was initially on, the TextField displays the unmodified name value.
The following test works.
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
init() {
self.name = "gradeItem" + UUID().uuidString
self.scoredPoints = 0.0
self.totalPoints = 0.0
self.isUserCreated = false
}
}
class Course: ObservableObject {
#Published var name: String
#Published var categories: [GradeCategory]
init(name: String, categories: [GradeCategory]) {
self.name = name
self.categories = categories
}
init() {
self.name = "course_" + UUID().uuidString
self.categories = [GradeCategory]()
self.categories.append(GradeCategory())
}
}
class GradeCategory: ObservableObject {
#Published var name: String
#Published var items: [GradeItem]
init(name: String, items: [GradeItem]) {
self.name = name
self.items = items
}
init() {
self.name = "category_" + UUID().uuidString
self.items = [GradeItem]()
self.items.append(GradeItem())
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id: \.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct CourseView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var course: Course // passed in from ContentView
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("done")
})
Spacer()
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
Spacer()
}.padding(.leading).frame(alignment: .leading)
}
}
struct ContentView: View {
#State var courses: [Course] = [Course(), Course()]
#State var selectedCourseIndex: Int = 0
#State var showCourse = false
var body: some View {
VStack {
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
self.showCourse = true
}, label: {
Text(self.courses[i].name)
})
}
}.sheet(isPresented: self.$showCourse) {
CourseView(course: self.courses[self.selectedCourseIndex])
}
}
}