Check multiple #Published values - swift

I have 3 classes:
class ClassOne: ObservableObject {
#Published var loading: Bool = false
}
class ClassTwo: ObservableObject {
#Published var loading: Bool = false
}
class ClassThree: ObservableObject {
#Published var loading: Bool = false
}
In a SwiftUI view I need to do something when all loading variables are true
This is a simplified version of my files of course: loading var of every class is set true or false by a download method.
I just need something to check if all download are completed and remove the loading view.
struct MainScreen3: View {
#State private var cancellables = Set<AnyCancellable>()
#EnvironmentObject var classOne: ClassOne
#EnvironmentObject var classTwo: ClassTwo
#EnvironmentObject var classThree: ClassThree
#State private var loading: Bool = true
var body: some View {
VStack {
if loading {
Text("Please wait...")
} else {
Text("Done!")
}
}.onAppear {
self.classOne.fetchFromServer()
self.classTwo.fetchFromServer()
self.classThree.fetchFromServer()
}
}
}
Any suggestion?

You can use the computed property.
private var loading: Bool {
(self.classOne.loading || self.classTwo.loading || self.classThree.loading)
}

You can use combineLatest to combine all 3 loading values into a single Publisher. You can subscribe to this publisher using onReceive on the view and update the existing loading State property to trigger a UI update.
.onReceive(classOne.$loading.combineLatest(classTwo.$loading, classThree.$loading, { $0 && $1 && $2 })) { loading in
self.loading = loading
}

Related

Updating SwiftUI View Based on ViewModel States?

I had a setup using #State in my SwiftUI view and going all my operations in the View (loading API etc) however when attempting to restructure this away from using #ViewBuilder and #State and using a #ObservedObject ViewModel, I lost the ability to dynamically change my view based on the #State variables
My code is now
#ObservedObject private var contentViewModel: ContentViewModel
init(viewModel: ContentViewModel) {
self.contentViewModel = viewModel
}
var body: some View {
if contentViewModel.isLoading {
loadingView
}
else if contentViewModel.fetchError != nil {
errorView
}
else if contentViewModel.movies.isEmpty {
emptyListView
} else {
moviesList
}
}
However whenever these viewmodel properties change, the view doesn't update like it did when i used them in the class as #State properties...
ViewModel is as follows:
final class ContentViewModel: ObservableObject {
var movies: [Movie] = []
var isLoading: Bool = false
var fetchError: String?
private let dataLoader: DataLoaderProtocol
init(dataLoader: DataLoaderProtocol = DataLoader()) {
self.dataLoader = dataLoader
fetch()
}
func fetch() {
isLoading = true
dataLoader.loadMovies { [weak self] result, error in
guard let self = `self` else { return }
self.isLoading = false
guard let result = result else {
return print("api error fetching")
}
guard let error = result.errorMessage, error != "" else {
return self.movies = result.items
}
return self.fetchError = error
}
}
How can i bind these 3 state deciding properties to View outcomes now they are abstracted away to a viewmodel?
Thanks
Place #Published before all 3 of your properties like so:
#Published var movies: [Movie] = []
#Published var isLoading: Bool = false
#Published var fetchError: String?
You were almost there by making the class conform to ObservableObject but by itself that does nothing. You then need to make sure the updates are sent automatically by using the #Published as I showed above or manually send the objectWillChange.send()
Edit:
Also you should know that if you pass that data down to any children you should make the parents property be #StateObject and the children's be ObservedObject

Two way binding to multiple instances

I have a view with a ForEach with multiple instances of another view. I want to be able to:
Click a button in the main view and trigger validation on the nested views, and
On the way back, populate an array with the results of the validations
I've simplified the project so it can be reproduced. Here's what I have:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
#Published var isValid : Bool
#Published var doValidate: Bool {
didSet{
print(doValidate) // This is never called
validate()
}
}
init(item : String, isValid : Bool, validate: Bool) {
self.item = item
self.isValid = isValid
self.doValidate = validate
}
func validate() { // This is never called
isValid = Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
var body: some View {
Text(viewModel.item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3", "nope"]
#Published var countersValidationResults = [Bool]()
#Published var performValidation = false
init() {
for _ in array {
countersValidationResults.append(false)
}
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(
item: viewModel.array[i],
isValid: viewModel.countersValidationResults[i],
validate: viewModel.performValidation
)
)
}
Button(action: {
viewModel.performValidation = true
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
When I change the property in the main view, the property doesn't change in the nested views, even though it's a #Published property.
Since this first step is not working, I haven't even been able to test the second part (updating the array of books with the validation results)
I need the setup to be like this because if an item is not valid, that view will show an error message, so the embedded views need to know whether they are valid or not.
UPDATE:
My issue was that you can't seem to be able to store Binding objects in view models, only in views, so I moved my properties to the view, and it works:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
init(item : String) {
self.item = item
print("item",item)
}
func validate() -> Bool{
return Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
#Binding var doValidate: Bool
#Binding var isValid : Bool
init(viewModel: AddEditItemViewModel, doValidate:Binding<Bool>, isValid : Binding<Bool>) {
self.viewModel = viewModel
self._doValidate = doValidate
self._isValid = isValid
}
var body: some View {
Text("\(viewModel.item): \(isValid.description)").onChange(of: doValidate) { _ in isValid = viewModel.validate() }
}
}
struct ContentView: View {
#State var performValidation = false
#State var countersValidationResults = [false,false,false,false] // had to hard code this here
#State var result : Bool = false
let array = ["1", "2", "3", "nope"]
// init() {
// for _ in array {
// countersValidationResults.append(false) // For some weird reason this appending doesn't happen!
// }
// }
var body: some View {
VStack {
ForEach(array.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(item: array[i]), doValidate: $performValidation, isValid: $countersValidationResults[i])
}
Button(action: {
performValidation.toggle()
result = countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
Text(countersValidationResults.description)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm having trouble reconciling the question with the example code and figuring out what's supposed to be happening. Think that there are a few issues going on.
didSet will not get called on #Published properties. You can (SwiftUI - is it possible to get didSet to fire when changing a #Published struct?) but the gist is that it's not a normal property, because of the #propertyWrapper around it
You say in your question that you want a "binding", but you never in fact us a Binding. If you did want to bind the properties together, you should look into using either #Binding or creating a binding without the property wrapper. Here's some additional reading on that: https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/
You have some circular logic in your example code. Like I said, it's a little hard to figure out what's a symptom of the code and what you're really trying to achieve. Here's an example that strips away a lot of the extraneous stuff going on and functions:
struct AddEditItemView: View {
var item : String
var isValid : Bool
var body: some View {
Text(item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3"]// "nope"]
#Published var countersValidationResults = [Bool]()
init() {
for _ in array {
countersValidationResults.append(false)
}
}
func validate(index: Int) { // This is never called
countersValidationResults[index] = Int(array[index]) != nil
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(item: viewModel.array[i], isValid: viewModel.countersValidationResults[i])
}
Button(action: {
viewModel.array.enumerated().forEach { (index,_) in
viewModel.validate(index: index)
}
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
Note that it your array, if you include the "nope" item, not everything validates, since there's a non-number, and if you omit it, everything validates.
In your case, there really wasn't the need for that second view model on the detail view. And, if you did have it, at least the way you had things written, it would have gotten you into a recursive loop, as it would've validated, then refreshed the #Published property on the parent view, which would've triggered the list to be refreshed, etc.
If you did get in a situation where you needed to communicate between two view models, you can do that by passing a Binding to the parent's #Published property by using the $ operator:
class ViewModel : ObservableObject {
#Published var isValid = false
}
struct ContentView : View {
#ObservedObject var viewModel : ViewModel
var body: some View {
VStack {
ChildView(viewModel: ChildViewModel(isValid: $viewModel.isValid))
}
}
}
class ChildViewModel : ObservableObject {
var isValid : Binding<Bool>
init(isValid: Binding<Bool>) {
self.isValid = isValid
}
func toggle() {
isValid.wrappedValue.toggle()
}
}
struct ChildView : View {
#ObservedObject var viewModel : ChildViewModel
var body: some View {
VStack {
Text("Valid: \(viewModel.isValid.wrappedValue ? "true" : "false")")
Button(action: {
viewModel.toggle()
}) {
Text("Toggle")
}
}
}
}

pass down a published var from an generic interactor that conforms to protocol

I am using SomeView in many places with different interactors, so it uses a general interactor that conforms to protocol InteractorProtocol. The problem is SomeView has several SomeButton views which take #Binding as argument and I can't pass down my someState1 and someState2 variables to SomeButton. I could pass my Interactor down to SomeButton and use the interactor variables there, but it feels wrong. Is there a way around this? Could the solution for the interactor maybe be different to make this work?
protocol InteractorProtocol {
var someState1: Bool { get set }
var someState1Published: Published<Bool> { get }
var someState1Publisher: Published<Bool>.Publisher { get }
var someState2: Bool { get set }
var someState2Published: Published<Bool> { get }
var someState2Publisher: Published<Bool>.Publisher { get }
}
class SomeInteractor: ObservableObject & InteractorProtocol {
#Published var someState1 = true
var someState1Published: Published<Bool> { _someState1 }
var someState1Publisher: Published<Bool>.Publisher { $someState1 }
#Published var someState2 = true
var someState2Published: Published<Bool> { _someState2 }
var someState2Publisher: Published<Bool>.Publisher { $someState2 }
}
struct SomeView<Interactor: InteractorProtocol & ObservableObject>: View {
#ObservedObject var interactor: Interactor
var body: some View {
HStack {
SomeButton(selected: self.interactor.$someState1) // not allowed
SomeButton(selected: self.interactor.$someState2) // not allowed
}
}
}
struct SomeButton: View {
#Binding var selected: Bool
var body: some View {
Text("text...")
.background(selected ? Color.green : Color.red)
}
}
The following should work:
protocol InteractorProtocol: ObservableObject {
var someState1: Bool { get set }
// ...
}
class SomeInteractor: InteractorProtocol {
#Published var someState1: Bool = true
// ...
}
struct SomeView<Interactor: InteractorProtocol>: View {
#ObservedObject var interactor: Interactor
var body: some View {
HStack {
SomeButton(selected: self.$interactor.someState1)
}
}
}

Change on ObservableObject in #Published array does not update the view

I've been struggling for hours on an issue with SwiftUI.
Here is a simplified example of my issue :
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.objectWillChange.send()
self.name = "Loaded name"
}
}
}
struct ContentView: View {
#ObservedObject var parent = Parent()
var body: some View {
Text(parent.children.first?.name ?? "null")
.onTapGesture {
self.parent.objectWillChange.send()
self.parent.children.first?.loadName() // does not update
}
}
}
I have an ObservableObject (Parent) storing a #Published array of ObservableObjects (Child).
The issue is that when the name property is changed via an async task on one object in the array, the view is not updated.
Do you have any idea ?
Many thanks
Nicolas
I would say it is design issue. Please find below preferable approach that uses just pure SwiftUI feature and does not require any workarounds. The key idea is decomposition and explicit dependency injection for "view-view model".
Tested with Xcode 11.4 / iOS 13.4
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.name = "Loaded name"
}
}
}
struct FirstChildView: View {
#ObservedObject var child: Child
var body: some View {
Text(child.name ?? "null")
.onTapGesture {
self.child.loadName()
}
}
}
struct ParentContentView: View {
#ObservedObject var parent = Parent()
var body: some View {
// just for demo, in real might be conditional or other UI design
// when no child is yet available
FirstChildView(child: parent.children.first ?? Child())
}
}
Make sure your Child model is a struct! Classes doesn't update the UI properly.
this alternative approach works for me:
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName(handler: #escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.name = UUID().uuidString // just for testing
handler()
}
}
}
struct ContentView8: View {
#ObservedObject var parent = Parent()
var body: some View {
Text(parent.children.first?.name ?? "null").padding(10).border(Color.black)
.onTapGesture {
self.parent.children.first?.loadName(){
self.parent.objectWillChange.send()
}
}
}
}

How to implement derived property using Combine?

I have an observable object with two properties
class Demo: ObservableObject {
#Published var propertyA: Bool
#Published var propertyB: Bool
}
Now I want to add a derived property "propertyC" that is "true" if both propertyA and propertyB are true.
I found similar questions with answers that didn't satisfy me. I'm looking for a solution that uses the Combine Framework and not a "didSet" method as my real world project computes the derived property from more than two other properties.
When I'm using the derived propertyC in a SwiftUI view it should trigger a refresh whenever propertyA or propertyB changes even if I don't use those in the view.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
class Demo: ObservableObject {
#Published var propertyA: Bool = false
#Published var propertyB: Bool = false
#Published var propertyC: Bool = false
private var subscribers = Set<AnyCancellable>()
init() {
Publishers.CombineLatest(_propertyA.projectedValue, _propertyB.projectedValue)
.receive(on: RunLoop.main)
.map { $0 && $1 }
.assign(to: \.propertyC, on: self)
.store(in: &subscribers)
}
}
// view for testing, works in Preview as well
struct FastDemoTest: View {
#ObservedObject var demo = Demo()
var body: some View {
VStack {
Button("Toggle A") { self.demo.propertyA.toggle() }
Button("Toggle B") { self.demo.propertyB.toggle() }
Divider()
Text("Result of C: \( demo.propertyC ? "true" : "false" )")
}
}
}
All credits should go Rob (see his notes to this question)
class Demo: ObservableObject {
#Published var propertyA: Bool = false
#Published var propertyB: Bool = false
var propertyC: Bool {
propertyA && propertyB
}
}
struct ContentView: View {
#ObservedObject var demo = Demo()
var body: some View {
VStack {
Button("Toggle A = \(demo.propertyA.description)") { self.demo.propertyA.toggle() }
Button("Toggle B = \(demo.propertyB.description)") { self.demo.propertyB.toggle() }
Divider()
Text("Result of C: \(demo.propertyC.description)")
}
}
}