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()
}
}
Related
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.
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()
}
}
}
I've got an #Published protocol array that I am looping through with a ForEach to present the elements in some view. I'd like to be able to use SwiftUI bindable syntax with the ForEach to generate a binding for me so I can mutate each element and have it reflected in the original array.
This seems to work for the properties that are implemented in the protocol, but I am unsure how I would go about accessing properties that are unique to the protocol's conforming type. In the example code below, that would be the Animal's owner property or the Human's age property. I figured some sort of type casting might be necessary, but can't figure out how to retain the reference to the underlying array via the binding.
Let me know if you need more detail.
import SwiftUI
protocol Testable {
var id: UUID { get }
var name: String { get set }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
}
class ContentViewModel: ObservableObject {
#Published var animalsAndHumans: [Testable] = []
}
struct ContentView: View {
#StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
ForEach($vm.animalsAndHumans, id: \AnyTestable.id) { $object in
TextField("textfield", text: $object.name)
// if the object is an Animal, how can I get it's owner?
}
Button("Add animal") {
vm.animalsAndHumans.append(Animal(id: UUID(), name: "Mick", owner: "harry"))
}
Button("Add Human") {
vm.animalsAndHumans.append(Human(id: UUID(), name: "Ash", age: 26))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is a thorny problem with your data types.
If you can change your data types, you can make this easier to solve.
For example, maybe you can model your data like this instead, using an enum instead of a protocol to represent the variants:
struct Testable {
let id: UUID
var name: String
var variant: Variant
enum Variant {
case animal(Animal)
case human(Human)
}
struct Animal {
var owner: String
}
struct Human {
var age: Int
}
}
It will also help to add accessors for the two variants' associated data:
extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}
var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}
Then you can write your view like this:
class ContentViewModel: ObservableObject {
#Published var testables: [Testable] = []
}
struct ContentView: View {
#StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: \.id) { $testable in
VStack {
TextField("Name", text: $testable.name)
if let human = Binding($testable.human) {
Stepper("Age: \(human.wrappedValue.age)", value: human.age)
}
else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}
HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}
Simple way to solve is extending your Testable protocol. Something likes
protocol Testable {
var id: UUID { get }
var name: String { get set }
var extra: String? { get }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
var extra: String? { nil }
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
var extra: String? { return owner }
}
Your ForEach block doesn't need to know the concrete type: Animal or Human, just check the extra of Testable to decide to add new element or not.
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)
}
}
}
}
I've been trying a simple feature to add new entries to a List. The View will just add a new generated. item (no need for user input).
struct PeopleList: View {
#ObservedObject var people: PersonStore
var body: some View {
NavigationView {
VStack {
Section {
Button(action: add) {
Text("Add")
}
}
Section {
List {
ForEach(people.people) { person in
NavigationLink(destination: PersonDetail(person: person)) {
PersonRow(person: person)
}
}
}
}
}
}
.navigationBarTitle(Text("People"))
.listStyle(GroupedListStyle())
}
func add() {
let newID = (people.people.last?.id ?? 0) + 1
self.people.people.append(Person(id: newID, name: ""))
}
}
This used to work in previous betas, but for some reason it's not working anymore. When I click on Add, the App does call the add() function and adds the new entry to the array, but the View is not updated at all.
These are the support classes:
class PersonStore: ObservableObject {
var people: [Person] {
willSet {
willChange.send()
}
}
init(people: [Person] = []) {
self.people = people
}
var willChange = PassthroughSubject<Void, Never>()
}
class Person: ObservableObject, Identifiable {
var id: Int = 0 {
willSet {
willChange.send()
}
}
var name: String {
willSet {
willChange.send()
}
}
init(id: Int, name: String) {
self.id = id
self.name = name
}
var willChange = PassthroughSubject<Void, Never>()
}
#if DEBUG
let data = [
Person(id: 1, name: "David"),
Person(id: 2, name: "Anne"),
Person(id: 3, name: "Carl"),
Person(id: 4, name: "Amy"),
Person(id: 5, name: "Daisy"),
Person(id: 6, name: "Mike"),
]
#endif
And the support views:
struct PersonRow: View {
#ObservedObject var person: Person
var body: some View {
HStack {
Image(systemName: "\(person.id).circle")
Text(person.name)
}.font(.title)
}
}
struct PersonDetail: View {
#ObservedObject var person: Person
var body: some View {
HStack {
Text("This is \(person.name)")
}.font(.title)
}
}
I've found someone with a problem that looks a bit related here:
SwiftUI: View content not reloading if #ObservedObject is a subclass of UIViewController. Is this a bug or am I missing something?
and here:
SwiftUI #Binding doesn't refresh View
The problem is that when you implemented your own ObservableObject you used the wrong publisher. The ObservableObject protocol creates the objectWillChange publisher but you never use it so that SwiftUI is never informed that anything has changed.
class Person: ObservableObject, Identifiable {
let id: Int
#Published
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
I haven't run that through the compiler so there might be some typos.
You don't have to use #Published but for a simple case like yours it's easier. Of course you need to update your other class as well.
Another small thing, id is required to never change, List et al. use it to connect your data with the view they create.