I'm playing with SwiftUI, trying to understand how ObservableObject works. I have an array of Person objects. When I add a new Person into the array, it is reloaded in my View, however if I change the value of an existing Person, it is not reloaded in the View.
// NamesClass.swift
import Foundation
import SwiftUI
import Combine
class Person: ObservableObject,Identifiable{
var id: Int
#Published var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
class People: ObservableObject{
#Published var people: [Person]
init(){
self.people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]
}
}
struct ContentView: View {
#ObservedObject var mypeople: People
var body: some View {
VStack{
ForEach(mypeople.people){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.people[0].name="Jaime"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
If I uncomment the line to add a new Person (John), the name of Jaime is shown properly, however if I just change the name this is not shown in the View.
I'm afraid I'm doing something wrong or maybe I don't get how the ObservedObjects work with arrays.
You can use a struct instead of a class. Because of a struct's value semantics, a change to a person's name is seen as a change to Person struct itself, and this change is also a change to the people array so #Published will send the notification and the View body will be recomputed.
import Foundation
import SwiftUI
import Combine
struct Person: Identifiable{
var id: Int
var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
class Model: ObservableObject{
#Published var people: [Person]
init(){
self.people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack{
ForEach(model.people){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
}
}
}
Alternatively (and not recommended), Person is a class, so it is a reference type. When it changes, the People array remains unchanged and so nothing is emitted by the subject. However, you can manually call it, to let it know:
Button(action: {
self.mypeople.objectWillChange.send()
self.mypeople.people[0].name="Jaime"
}) {
Text("Add/Change name")
}
I think there is a more elegant solution to this problem. Instead of trying to propagate the objectWillChange message up the model hierarchy, you can create a custom view for the list rows so each item is an #ObservedObject:
struct PersonRow: View {
#ObservedObject var person: Person
var body: some View {
Text(person.name)
}
}
struct ContentView: View {
#ObservedObject var mypeople: People
var body: some View {
VStack{
ForEach(mypeople.people){ person in
PersonRow(person: person)
}
Button(action: {
self.mypeople.people[0].name="Jaime"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
In general, creating a custom view for the items in a List/ForEach allows each item in the collection to be monitored for changes.
For those who might find it helpful. This is a more generic approach to #kontiki 's answer.
This way you will not have to be repeating yourself for different model class types
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
#Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<T: ObservableObject>() -> ObservableArray<T> {
let array2 = array as! [T]
array2.forEach({
let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self as! ObservableArray<T>
}
}
class Person: ObservableObject,Identifiable{
var id: Int
#Published var name: String
init(id: Int, name: String){
self.id = id
self.name = name
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Person) to conform to ObservabeObject protocol
#ObservedObject var mypeople: ObservableArray<Person> = ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")])
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Person) must conform to ObservableObject protocol
#ObservedObject var mypeople: ObservableArray<Person> = try! ObservableArray(array: [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")]).observeChildrenChanges()
var body: some View {
VStack{
ForEach(mypeople.array){ person in
Text("\(person.name)")
}
Button(action: {
self.mypeople.array[0].name="Jaime"
//self.mypeople.people.append(Person(id: 5, name: "John"))
}) {
Text("Add/Change name")
}
}
}
}
ObservableArray is very useful, thank you! Here's a more generalised version that supports all Collections, which is handy when you need to react to CoreData values indirected through a to-many relationship (which are modelled as Sets).
import Combine
import SwiftUI
private class ObservedObjectCollectionBox<Element>: ObservableObject where Element: ObservableObject {
private var subscription: AnyCancellable?
init(_ wrappedValue: AnyCollection<Element>) {
self.reset(wrappedValue)
}
func reset(_ newValue: AnyCollection<Element>) {
self.subscription = Publishers.MergeMany(newValue.map{ $0.objectWillChange })
.eraseToAnyPublisher()
.sink { _ in
self.objectWillChange.send()
}
}
}
#propertyWrapper
public struct ObservedObjectCollection<Element>: DynamicProperty where Element: ObservableObject {
public var wrappedValue: AnyCollection<Element> {
didSet {
if isKnownUniquelyReferenced(&observed) {
self.observed.reset(wrappedValue)
} else {
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
}
}
#ObservedObject private var observed: ObservedObjectCollectionBox<Element>
public init(wrappedValue: AnyCollection<Element>) {
self.wrappedValue = wrappedValue
self.observed = ObservedObjectCollectionBox(wrappedValue)
}
public init(wrappedValue: AnyCollection<Element>?) {
self.init(wrappedValue: wrappedValue ?? AnyCollection([]))
}
public init<C: Collection>(wrappedValue: C) where C.Element == Element {
self.init(wrappedValue: AnyCollection(wrappedValue))
}
public init<C: Collection>(wrappedValue: C?) where C.Element == Element {
if let wrappedValue = wrappedValue {
self.init(wrappedValue: wrappedValue)
} else {
self.init(wrappedValue: AnyCollection([]))
}
}
}
It can be used as follows, let's say for example we have a class Fridge that contains a Set and our view needs to react to changes in the latter despite not having any subviews that observe each item.
class Food: ObservableObject, Hashable {
#Published var name: String
#Published var calories: Float
init(name: String, calories: Float) {
self.name = name
self.calories = calories
}
static func ==(lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name && lhs.calories == rhs.calories
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.name)
hasher.combine(self.calories)
}
}
class Fridge: ObservableObject {
#Published var food: Set<Food>
init(food: Set<Food>) {
self.food = food
}
}
struct FridgeCaloriesView: View {
#ObservedObjectCollection var food: AnyCollection<Food>
init(fridge: Fridge) {
self._food = ObservedObjectCollection(wrappedValue: fridge.food)
}
var totalCalories: Float {
self.food.map { $0.calories }.reduce(0, +)
}
var body: some View {
Text("Total calories in fridge: \(totalCalories)")
}
}
The ideal thing to do would be to chain #ObservedObject or #StateObject and some other property wrapper that is suitable for sequences, e.g. #StateObject #ObservableObjects. But you can't use more than one property wrapper, so you need to make different types to handle the two different cases. Then you can use either one of the following, as appropriate.
(Your People type is unnecessary—its purpose can be abstracted to all sequences.)
#StateObjects var people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")
]
#ObservedObjects var people: [Person]
import Combine
import SwiftUI
#propertyWrapper
public final class ObservableObjects<Objects: Sequence>: ObservableObject
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
self.wrappedValue = wrappedValue
assignCancellable()
}
#Published public var wrappedValue: Objects {
didSet { assignCancellable() }
}
private var cancellable: AnyCancellable!
}
// MARK: - private
private extension ObservableObjects {
func assignCancellable() {
cancellable = Publishers.MergeMany(wrappedValue.map(\.objectWillChange))
.sink { [unowned self] _ in objectWillChange.send() }
}
}
// MARK: -
#propertyWrapper
public struct ObservedObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
#ObservedObject private var objects: ObservableObjects<Objects>
}
#propertyWrapper
public struct StateObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
#StateObject private var objects: ObservableObjects<Objects>
}
Related
let's say you have a list of some objects - let's say cats. There is a common viewmodel that stores the data of the cats received from the server. Each element on the view is a small sector of data for one cat. Within this sector, the data can be changed by the user. How to correctly link the viewmodel of the list of cats and the viewmodel of one cat?
struct Cat : Identifiable {
let id = UUID()
var name: String
var breed: String
}
struct CatListView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
List(self.viewModel.items) { (item) in
CatView(viewModel: self.viewModel.createViewModel(item: item))
}
}
}
extension CatListView {
class ViewModel : ObservableObject {
#Published var items: [Cat]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
.init(data: item)
}
}
}
struct CatView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
VStack {
TextField("Name", text: self.$viewModel.data.name)
TextField("Breed", text: self.$viewModel.data.breed)
}
}
}
extension CatView {
class ViewModel : ObservableObject {
#Published var data: Cat
init(data: Cat) {
self._data = .init(initialValue: data)
}
}
}
How to properly transfer changes from CatView.ViewModel to CatListView.ViewModel?
Is Binding possible? But I think this is a variant of the connection between View and View.
Perhaps this option will be correct?
extension CatListView {
class ViewModel : ObservableObject {
#Published private(set) var items: [Cat]
private var cancelItems: [Int: AnyCancellable] = [:]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
let model: CatView.ViewModel = .init(data: item)
if let index = self.items.firstIndex(where: { $0.id == item.id }) {
self.cancelItems[index] = model.$data
.dropFirst()
.removeDuplicates()
.sink {
self.items[index] = $0
}
}
return model
}
}
}
You have to manage these items:
Model: single Cat Model
ViewModel: Cat Collection Model that stores all cats and fetch them
View 1 with cats List
View 2 (optional) A View with single cat, can be inside View 1 or separate, but for illustrate this we will separate the View 2.
struct Cat: Identifiable, Hashable {
let id = UUID()
var name: String
var colorDescription: String
}
class CatsModel: ObservableObject {
#Published var catsBag: [Cat]
init() {
// Fetch here the cats from your server
}
// CRUD cicle
func create(_ cat: Cat) { ... }
func update(_ cat: Cat) { ... }
func delete(_ cat: Cat) { ... }
}
struct CatsList: View {
#StateObject var catsModel = CatsModel()
var body: some View {
List {
ForEach(catsModel.catsBag.indices, id: \.self) { catIndice in
CatCell(cat: $catsModel.catsBag[catIndice])
}
}
}
}
struct CatCell: View {
#Binding var cat: Cat
var body: some View {
TextField("The cat name is: ", text: $cat.name)
.padding()
}
}
You will appreciate at this point that if you delete the last cat of that list the app crashes. This could be a SwiftUI error, you can find the solution here: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
I'm unsure why my view isn't getting updated when I have a view model nested inside another. My understanding was that a #Published property in the child view model would trigger a change in the parent viewModel, causing that to push changes to the UI.
This is the child view model:
class FilterViewModel : ObservableObject, Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
#Published var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
func changeSelected() {
self.selected = !self.selected
}
}
The following works as expected, on clicking the button the background colour is changed.
struct ContentView: View {
#ObservedObject var filterVM = FilterViewModel(name: "A")
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
However, I want to have an array of filter view models so tried:
class FilterListViewModel: ObservableObject {
#Published var filtersVMS = [FilterViewModel]()
init(){
filtersVMS = [
FilterViewModel(name: "A"),
FilterViewModel(name: "B"),
FilterViewModel(name: "C"),
FilterViewModel(name: "D")
]
}
}
However, the following view is not updated when clicking the button
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
Button(action: { filterListVM[0].changeSelected()}, label: {
Text(filterListVM[0].name)
.background(Color(filterListVM[0].backgroundColour))
})
}
Alternate solution is to use separated view for your sub-model:
struct FilterView: View {
#ObservedObject var filterVM: FilterViewModel
var body: some View {
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
}
so in parent view now we can just use it as
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
// ...
FilterView(filterVM: filterListVM[0])
}
The most simplest way is to define your FilterViewModel as struct. Hence, it is a value type. When a value changes, the struct changes. Then your ListViewModel triggers a change.
struct FilterViewModel : Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
mutating func changeSelected() {
self.selected = !self.selected
}
}
I'm working with Picker in SwiftUI to choose from a list of Core Data NSManagedObjects, and can't get the picker to display a default value. It also won't set a new value after one is chosen. Is there a way to have a default value for the picker to display?
Here's where the properties for my NSManagedObject are set up.
extension Company {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Company> {
return NSFetchRequest<Company>(entityName: "Company")
}
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var companyContacts: NSSet?
#NSManaged public var companyRoles: NSSet?
//...
}
And here's where I'm trying to use it.
struct AddRoleSheet: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(
entity: Company.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Company.name, ascending: true)
]
) var companies: FetchedResults<Company>
//...
#State var company: Company? = // Can I put something here? Would this solve my problem?
//...
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $company, label: Text("Company")) {
List {
ForEach(companies, id: \.self) { company in
company.name.map(Text.init)
}
}
}
//...
}
}
//...
}
}
There is no fetched results on View.init phase yet, so try the following
#State var company: Company? = nil // << just initialize
//...
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $company, label: Text("Company")) {
List {
ForEach(companies, id: \.self) { company in
company.name.map(Text.init)
}
}
}
//...
}
}
}.onAppear {
// here companies already fetched from database
self.company = self.companies.first // << assign any needed
}
}
I'm trying to work out how I can correctly pass an object or a set of values between two ViewModels in a parent-child relationship so that when the child ViewModel is updated the change bubbles back up to the parent.
This is pretty simple when just using SwiftUI views and binding directly to the stores but I wanted to keep my business logic for field validation and so on separate from the SwiftUI views.
The code below shows the child updating (as expected) when the parent gets updated, but I need to somehow pass the changed values in the child back up to the parent. I'm very new to mobile app development and still learning so I'm sure I'm missing something quite simple.
import SwiftUI
import Combine
struct Person: Hashable {
var givenName: String
var familyName: String
}
// my person store - in the real app it's backed by coredata
class PersonStore: ObservableObject {
#Published var people: [Person] = [
Person(
givenName: "Test",
familyName: "Person"
)
]
static let shared = PersonStore()
}
// app entrypoint
struct PersonView: View {
#ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.people.indices, id: \.self) { idx in
NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(personIndex: idx))) {
Text(self.viewModel.people[idx].givenName)
}
}
}
}
}
}
class PersonView_ViewModel: ObservableObject {
#Published var people: [Person] = PersonStore.shared.people
}
// this is the detail view
struct PersonDetailView: View {
#ObservedObject var viewModel: PersonDetailView_ViewModel
var body: some View {
Form {
Section(header: Text("Parent View")) {
VStack {
TextField("Given Name", text: self.$viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.$viewModel.person.familyName)
}
}
PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
}
}
}
// viewmodel associated with detail view
class PersonDetailView_ViewModel: ObservableObject {
#Published var person: Person
init(personIndex: Int) {
self.person = PersonStore.shared.people[personIndex]
}
}
// this is the child view - in the real app there are multiple sections which are conditionally rendered
struct PersonBasicDetails: View {
#ObservedObject var viewModel: PersonBasicDetails_ViewModel
var body: some View {
Section(header: Text("Child View")) {
VStack {
TextField("Given Name", text: self.$viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.$viewModel.person.familyName)
}
}
}
}
class PersonBasicDetails_ViewModel: ObservableObject {
#Published var person: Person
init(person: Person) {
self.person = person
}
}
struct PersonView_Previews: PreviewProvider {
static var previews: some View {
PersonView()
}
}
In most SwiftUI TextField examples around the web the binding is provided by utilizing a #State variable which creates an instance of Binding for you.
However, you can also create a custom binding using the Binding constructor. Here's an example of what that looks like:
TextField(
"Given Name",
text: Binding(
get: { self.$viewModel.person.givenName },
set: { self.$viewModel.person.givenName = $0 }))
If you want two way works, not only you need to publish, also you have to use binding for upward.
struct Person: Hashable {
var givenName: String
var familyName: String
}
// my person store - in the real app it's backed by coredata
class PersonStore: ObservableObject {
#Published var people: [Person] = [
Person(givenName: "Test",familyName: "Person")
]
static let shared = PersonStore()
}
// app entrypoint
struct PersonView: View {
#ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.people.indices, id: \.self) { idx in
NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(person: self.$viewModel.people , index: idx ))) {
Text(self.viewModel.people[idx].givenName)
}
}
}
}
}
}
class PersonView_ViewModel: ObservableObject {
#Published var people: [Person] = PersonStore.shared.people
}
// this is the detail view
struct PersonDetailView: View {
#ObservedObject var viewModel: PersonDetailView_ViewModel
var body: some View {
Form {
Section(header: Text("Parent View")) {
VStack {
TextField("Given Name", text: self.viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.viewModel.person.familyName)
}
}
PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
}
}
}
// viewmodel associated with detail view
class PersonDetailView_ViewModel: ObservableObject {
#Published var person: Binding<Person>
init(person: Binding<[Person]> ,index: Int) {
self.person = person[index]
}
}
// this is the child view - in the real app there are multiple sections which are conditionally rendered
struct PersonBasicDetails: View {
#ObservedObject var viewModel: PersonBasicDetails_ViewModel
var body: some View {
Section(header: Text("Child View")) {
VStack {
TextField("Given Name", text: self.viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.viewModel.person.familyName)
}
}
}
}
class PersonBasicDetails_ViewModel: ObservableObject {
#Published var person: Binding<Person>
init(person: Binding<Person>) {
self.person = person //person
}
}
struct PersonView_Previews: PreviewProvider {
static var previews: some View {
PersonView()
}
}
So I'm getting the error Unknown attribute ObservableObject next to the #ObservableObject var dataSource = DataSource() call below. the ObservableObject worked perfectly a couple days ago in another project but not anymore.
import SwiftUI
import Combine
class DataSource: ObservableObject {
var willChange = PassthroughSubject<Void,Never>()
var expenses = [Expense]() {
willSet { willChange.send() }
}
var savingsItems = [SavingsItem](){
willSet { willChange.send() }
}
//#State var monthlyIncomeText: String
//var monthlyIncome: Int = 1364
init(){
addNewExpense(withName: "Spotify", price: 14)
}
func addNewExpense(withName name: String, price: Int){
let newExpense = Expense(name: name, price: price)
expenses.append(newExpense)
}
func addNewSavingsItem(withName name: String, price: Int, percentage: Double){
let newSavingsItem = SavingsItem(name: name, price: price, timeTilCompletion: 0, percentage: percentage)
savingsItems.append(newSavingsItem)
}
}
struct ContentView: View {
#ObservableObject var dataSource = DataSource()
var body: some View {
VStack{
Text("Expenses")
List(dataSource.expenses) { expense in
ExpenseRow(expense: expense)
}
}
}
}
Could anyone help?
ObservableObject is a protocol that ObservedObjects must conform to. See here for documentation on ObservableObject, and here for documentation on ObservedObject, which is the property wrapper that you are looking for. Change your ContentView code to this:
struct ContentView: View {
#ObservedObject var dataSource = DataSource()
var body: some View {
VStack {
Text("Expenses")
List(dataSource.expenses) { expense in
ExpenseRow(expense: expense)
}
}
}
}