How to implement derived property using Combine? - swift

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)")
}
}
}

Related

Is there a simpler way to zip two .onReceive in swiftui

#State private var showUpEmotion = false
#State private var showDownEmotion = false
When two pieces of data from an observableobject come online some view is shown
HStack {
if showUpEmotion && showDownEmotion
{
SomeViewIsShown()
}
}
.onReceive(model.$meLike) { value in
withAnimation {
if value != nil {
showUpEmotion = true
} else {
showUpEmotion = false
}
}
}
.onReceive(model.$meDislike) { value in
withAnimation {
if value != nil {
showDownEmotion = true
} else {
showDownEmotion = false
}
}
}
Is there a simpler/cleaner way to zip that data from ObservableObject ?
naturally withAnimation does not compile in the observableobject proper -> I have to use that inside the view :(
It's not clear from the question what you're trying to achieve, but possibly moving some of your Bools into structs would simplify?
struct ShowEmotions {
var up = false
var down = false
var both: Bool {
up && down
}
}
struct Likes {
var like = false
var dislike = false
}
class Model: ObservableObject {
#Published var me = Likes()
}
struct ContentView: View {
#State private var showEmotions = ShowEmotions()
#StateObject var model = Model()
var body: some View {
HStack {
if showEmotions.both {
SomeViewIsShown()
}
}
.onReceive(model.$me) { me in
withAnimation {
showEmotions.up = me.like
showEmotions.down = me.dislike
}
}
}
}
This way you only need a single onReceive

Check multiple #Published values

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
}

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")
}
}
}
}

In SwiftUI how can I animate a Published View change?

I would like to animate a View change.
In particular, I have done a View where one of its children is a changing View.
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
WaitPlayerBackView(isShowing: self.$playerLeft) {
self.viewModel.currentView
}
}
}
}
The view model is an ObservableObject class. currentView is changed by a method called from an async thread, like this:
class MasterViewModel: ObservableObject {
#Published var currentView: AnyView = AnyView(EmptyView())
func changeView() {
self.currentView = AnyView(NightView())
}
}
It's like a copy of Android fragments (pardon the comparison).
How can I animate this View change?
I have tried different options like:
self.viewModel.currentView.animate(.default)
or
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
WaitPlayerBackView(isShowing: self.$playerLeft) {
self.viewModel.currentView
}
}.animate(self.viewModel.animate ? .easeIn(duration: 1) : .none)
}
}
class MasterViewModel: ObservableObject {
#Published var currentView: AnyView = AnyView(EmptyView())
#Published var animate = false
func changeView() {
self.animate = false
self.currentView = AnyView(NightView())
self.animate = true
}
}
However, none of them worked.
This is how I see a transition when changing the instance of currentView.
Moving your view initialization to MasterView makes everything easier:
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
VStack { //this represents WaitPlayerBackView
if viewModel.day { //here are the posibles views that self.viewModel.currentView could have
Text("DayView").transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
} else {
Text("NightView").transition(AnyTransition.opacity.animation(.(duration: 1.0)))
}
}
}
}
}
class MasterViewModel: ObservableObject {
#Published var day: Bool = false
init () {
_ = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(changeView), userInfo: nil, repeats: true) //Only for testing
}
#objc func changeView() { //#objc is only for testing
day.toggle()
}
}

Two-way binding in Swift Combine

I have a progress bar and a text field, both are updated depending on each other's input:
class ViewModel: ObservableObject {
#Published var progressBarValue: Double {
didSet {
textFieldValue = String(progressBarValue)
}
}
#Published var textFieldValue: String {
didSet {
progressBarValue = Double(progressBarValue)
}
}
}
Since updating one updates the other, I end up having an infinite recursion in my code.
Is there a way to workaround this with Combine or plain swift code?
Expanding on my comment, here is a minimal example of a slider and a textfield that both control (and be controlled by) a value via two-way bindings:
class ViewModel: ObservableObject {
#Published var progress: Double = 0
}
struct ContentView: View {
#EnvironmentObject var model: ViewModel
var body: some View {
VStack {
TextField("", value: self.$model.progress, formatter: NumberFormatter())
Slider(value: self.$model.progress, in: 0...100)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ViewModel())
}
}
Note that I also had to inject a ViewModel instance to my environment on AppDelegate in order for this to work (both on preview & actual app)
Maybe additional checks to avoid loops will work?
#Published var progressBarValue: Double {
didSet {
let newText = String(progressBarValue)
if newText != textFieldValue {
textFieldValue = newText
}
}
}
#Published var textFieldValue: String {
didSet {
if let newProgress = Double(textFieldValue),
abs(newProgress - progressBarValue) > Double.ulpOfOne {
progressBarValue = newProgress
}
}
}