I have a StateController class:
import Foundation
import Combine
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
#Published var subjects: [Subject]
private let storageController = StorageController()
init() {
self.subjects = storageController.fetchData()
}
// MARK: - Computed properties
// Array with all tasks from subjects, computed property
var allTasks: [Task] {
var all: [Task] = []
for subject in subjects {
all += subject.tasks
}
print("Computed property updated!")
return all
}
var numberofCompletedTasks: Int {
return subjects.map({$0.tasks.map({$0.isCompleted == true})}).count
}
var numberOfHighPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 1})}).count
}
var numberOfMediumPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 2})}).count
}
var numberOfLowPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 3})}).count
}
}
And a SwiftUI view:
import SwiftUI
struct SmartList: View {
// MARK: - Properties
let title: String
#EnvironmentObject private var stateController: StateController
// MARK: - View body
var body: some View {
List(stateController.allTasks, id: \.taskID) { task in
TaskView(task: task)
.environmentObject(self.stateController)
}.listStyle(InsetGroupedListStyle())
.navigationTitle(LocalizedStringKey(title))
}
}
When I update "Task" objects inside "subjects" #Published array, for example checking them as complete, SwiftUI should automatically update the view because computed properties are derived from #Published property of an ObservableObject (declared as #EnvironmentObject inside view) but it doesn't work.
How can I bind my SwiftUI view to computed properties derived from a #Published property??
Sadly, SwiftUI automatically updating views that are displaying computed properties when an #EnvironmentObject/#ObservedObject changes only works in very limited circumstances. Namely, the #Published property itself cannot be a reference type, it needs to be a value type (or if it's a reference type, the whole reference needs to be replaced, simply updating a property of said reference type won't trigger an objectWillChange emission and hence a View reload).
Because of this, you cannot rely on computed properties with SwiftUI. Instead, you need to make all properties that your view needs stored properties and mark them as #Published too. Then you need to set up a subscription on the #Published property, whose value you need for your computed properties and hence update the value of your stored properties each time the value they depend on changes.
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
#Published var subjects: [Subject]
#Published var allTasks: [Task] = []
private let storageController = StorageController()
private var subscriptions = Set<AnyCancellable>()
init() {
self.subjects = storageController.fetchData()
// Closure for calculating the value of `allTasks`
let calculateAllTasks: (([Subject]) -> [Task]) = { subjects in subjects.flatMap { $0.tasks } }
// Subscribe to `$subjects`, so that each time it is updated, pass the new value to `calculateAllTasks` and then assign its output to `allTasks`
self.$subjects.map(calculateAllTasks).assign(to: \.allTasks, on: self).store(in: &subscriptions)
}
}
Related
Just in case, the Book struct is below.
struct Book: Identifiable {
var id = UUID().uuidString
var title: String
var description: String
var imageURL: URL
var sourceOfDesc: String
var descSourceCitation: String
}
My goal is to display a list of BookDetailView marked as favorites. A class to add and remove books from the favorites has been created.
class Favorites: ObservableObject {
// The actual books the user marked as favorite.
#Published var books: [String]
// The key to be used to read/write in the UserDefaults
private let saveKey = "Favorites"
init() {
// Load saved data
books = []
}
// Returns true if the set contains this book
func contains(_ book: Book) -> Bool {
books.contains(book.id)
}
func add(_ book: Book) {
objectWillChange.send()
books.insert(book.id, at: 0)
save()
}
func remove(_ book: Book) {
objectWillChange.send()
books.removeAll { $0 == book.id }
save()
}
func save() {
// Write data
}
}
Updated FavoritesView below.
struct FavoritesView: View {
#ObservedObject var favoriteList: Favorites
var book: Book
var body: some View {
List(favoriteList.books) { book in
NavigationLink {
WorksListTemplateView(books: book)
} label: {
Text(book.title)
}
}
}
}
I get multiple error messages on FavoritesView, the ones that jump out to me are these 2:
Cannot convert value of type '[String]' to expected argument type 'Binding'
Generic parameter 'Data' could not be inferred
List is excepting a RandomAccessCollection, Set doesn't not conform to it. So you should convert your set to an array: Array(favoritesList.books).
However, since String does not conform to Identifiable, you also need to add an id to the list:
List(Array(favoritesList.books), id: \.self) { book in
Remarks:
As mentioned in the comments, in Favorites you should mark books using #Published in order for ObservableObject to take effect:
#Published var books: Set<String> //or an array [String]
In FavoritesView, favoriteList should be:
#ObservedObject var favoriteList: Favorites
//ObservedObject & not StateObject because the object is passed
//down from another View & not directly initialized in the View.
In BookDetailView, you don't need .environmentObject(favorites). You inject it when you first Initialize it, i.e where you don't have #EnvironmentObject var favorites: Favorites or when you are presenting another View that needs it.
Also in BookDetailView, if you need to mutate book from its View mark it with #State. If you need to mutate book from another View, in that View mark it with #Binding.
I want to put the logic of all my #Published in a model class, however when I try to separate it, it doesn't update. I recreated a little example:
The code below works, it increases every time the button is clicked:
struct ContentView: View {
#StateObject var myClass = MyClass()
var body: some View {
Button(action: {
myClass.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
func doStuff(numb: Int) {
people += numb
}
}
However, once I split the logic and try to have my #Published in a separate class to have it more clean, it doesn't update, see below:
struct ContentView: View {
#StateObject var myClass = MyClass()
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass()
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
}
I think it's because there are two different instances in the view right? Anyway, how can I separate the #Publish correctly have it updated?
Thanks
Your first form is absolutely fine! You may, though, consider your ContentView using a #ObservedObject instead a #StateObject.
Your second form is flawed, for several reasons:
don't move logic into a view
don't use class variables to keep "state".
The first statement is due to a sane design that keeps your models and views nicely separated.
The second statement is due to how SwiftUI works. If you need to have some "state" in your views, use #State where the value is a struct.
Using #State ensures, that it's value is bound to the actual life time of the "conceptual view", i.e. the thing we human perceive as the view. And this "conceptual view" (managed as some data object by SwiftUI) is distinct from the struct View, which is merely a means to describe how to create and modify this conceptual view - that is, struct view is rather a function that will be used to initially create the "conceptual view" and modify it. Once this is done, it gets destroyed, and gets recreated when the conceptual view needs to be modified. That also means, the life time of this struct is not bound to the life time of its "state". The state's life time is bound to the conceptual view, and thus has usually longer life time than the struct view, where the struct view can be created and destroyed several times.
Now, imagine what happens when you always execute let modify = Modify() whenever the (conceptual) view or its content view is modified and needs to be recalculated and rendered by creating a struct view and then - after it has been rendered - destroying it again.
Also, this "state" is considered private for the view, means it is considered an implementation detail for the view. If you want to exchange data from "outside" and "inside" use a #ObservedObject or a Binding.
The problem is that you have 2 separate instances of MyClass:
#StateObject var myClass = MyClass()
var myClass = MyClass()
You are updating the myClass in Modify, which you aren't receiving updates from. A way to fix this is by having one instance of MyClass, passed into Modify during initialization:
struct ContentView: View {
#StateObject var myClass: MyClass
let modify: Modify
init() {
let temp = MyClass()
_myClass = StateObject(wrappedValue: temp)
modify = Modify(myClass: temp)
}
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
let myClass: MyClass
init(myClass: MyClass) {
self.myClass = myClass
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
Another method is to have a #Published property in Modify to observe the changes of MyClass:
struct ContentView: View {
#StateObject var modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(modify.myClass.people)")
}
}
}
class Modify: ObservableObject {
#Published var myClass = MyClass()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = myClass.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
you could try this approach using a singleton. Works well for me:
struct ContentView: View {
#StateObject var myClass = MyClass.shared // <--- here
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass.shared // <--- here
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
static let shared = MyClass() // <--- here
}
I have 2 independent ObservableObjects called ViewModel1 and ViewModel2.
ViewModel2 has an array of strings:
#Published var strings: [String] = [].
Whenever that array is modified i want ViewModel1 to be informed.
What's the recommended approach to achieve this?
Clearly, there are a number of potential solutions to this, like the aforementioned NotificationCenter and singleton ideas.
To me, this seems like a scenario where Combine would be rather useful:
import SwiftUI
import Combine
class ViewModel1 : ObservableObject {
var cancellable : AnyCancellable?
func connect(_ publisher: AnyPublisher<[String],Never>) {
cancellable = publisher.sink(receiveValue: { (newStrings) in
print(newStrings)
})
}
}
class ViewModel2 : ObservableObject {
#Published var strings: [String] = []
}
struct ContentView : View {
#ObservedObject private var vm1 = ViewModel1()
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
VStack {
Button("add item") {
vm2.strings.append("\(UUID().uuidString)")
}
ChildView(connect: vm1.connect)
}.onAppear {
vm1.connect(vm2.$strings.eraseToAnyPublisher())
}
}
}
struct ChildView : View {
var connect : (AnyPublisher<[String],Never>) -> Void
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
Button("Connect child publisher") {
connect(vm2.$strings.eraseToAnyPublisher())
vm2.strings = ["Other strings","From child view"]
}
}
}
To test this, first try pressing the "add item" button -- you'll see in the console that ViewModel1 receives the new values.
Then, try the Connect child publisher button -- now, the initial connection is cancelled and a new one is made to the child's iteration of ViewModel2.
In order for this scenario to work, you always have to have a reference to ViewModel1 and ViewModel2, or at the least, the connect method, as I demonstrated in ChildView. You could easily pass this via dependency injection or even through an EnvironmentObject
ViewModel1 could also be changed to instead of having 1 connection, having many by making cancellable a Set<AnyCancellable> and adding a connection each time if you needed a one->many scenario.
Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.
I had same problem and I found this method working well, just using the idea of reference type and taking advantage of class like using shared one!
import SwiftUI
struct ContentView: View {
#StateObject var viewModel2: ViewModel2 = ViewModel2.shared
#State var index: Int = Int()
var body: some View {
Button("update strings array of ViewModel2") {
viewModel2.strings.append("Hello" + index.description)
index += 1
}
}
}
class ViewModel1: ObservableObject {
static let shared: ViewModel1 = ViewModel1()
#Published var onReceiveViewModel2: Bool = Bool() {
didSet {
print("strings array of ViewModel2 got an update!")
print("new update is:", ViewModel2.shared.strings)
}
}
}
class ViewModel2: ObservableObject {
static let shared: ViewModel2 = ViewModel2()
#Published var strings: [String] = [String]() {
didSet { ViewModel1.shared.onReceiveViewModel2.toggle() }
}
}
Let's assume a model, which implements the protocol ObservableObject and has got a #Published property name.
// MARK: Model
class ContentSinglePropertyModel: ObservableObject {
#Published public var name: String
}
Now, I would like to display that name in a view and update the view, whenever name in the model changes. Additionally, I would like to use the Model-View-ViewModel (MVVM) pattern to achieve this goal.
// MARK: ViewModel
final class ContentSinglePropertyViewModel: ObservableObject {
private let model: ContentSinglePropertyModel
#Published var name: String = ""
init() {
self.model = ContentSinglePropertyModel()
}
}
// MARK: View
struct ContentSinglePropertyView: View {
#ObservedObject var viewModel: ContentSinglePropertyViewModel
var body: some View {
Text(self.viewModel.name)
}
}
Since I don't like the idea to make the model or it's properties public within the viewmodel, one option is to wrap the model's property name in the viewmodel. My question is: How to connect the name of the model and the viewmodel in the most idiomatic way?
I've came up with the solution to update the viewmodel's property through the use of Combine's assign method:
self.model.$name.assign(to: \.name, on: self).store(in: &self.cancellables)
Is there a better solution?
My working example:
import SwiftUI
import Combine
// MARK: Model
class ContentSinglePropertyModel: ObservableObject {
#Published public var name: String
init() {
self.name = "Initial value"
}
func doSomething() {
self.name = "Changed value"
}
}
// MARK: ViewModel
final class ContentSinglePropertyViewModel: ObservableObject {
private let model: ContentSinglePropertyModel
private var cancellables: Set<AnyCancellable> = []
#Published var name: String = ""
init() {
self.model = ContentSinglePropertyModel()
// glue Model and ViewModel
self.model.$name.assign(to: \.name, on: self).store(in: &self.cancellables)
}
func doSomething() {
self.model.doSomething()
}
}
// MARK: View
struct ContentSinglePropertyView: View {
#ObservedObject var viewModel: ContentSinglePropertyViewModel
var body: some View {
VStack {
Text(self.viewModel.name)
Button("Do something!", action: {
self.viewModel.doSomething()
})
}
}
}
struct ContentSinglePropertyView_Previews: PreviewProvider {
static var previews: some View {
ContentSinglePropertyView(viewModel: .init())
}
}
I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}