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

#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

Related

SwiftUI Custom Environment Value

I try to make custom environment key to read its value as shown in the code below, I read many resources about how to make it and all have the same approach.
Example Code
struct Custom_EnvironmentValues: View {
#State private var isSensitive = false
var body: some View {
VStack {
// Update the value here <---
Toggle(isSensitive ? "Sensitive": "Not sensitive", isOn: $isSensitive)
PasswordField(password: "123456")
.isSensitive(isSensitive)
}.padding()
}
}
struct PasswordField: View {
let password: String
#Environment(\.isSensitive) private var isSensitive
var body: some View {
HStack {
Text("Password")
Text(password)
// It should update the UI here but that not happened <---
.foregroundColor(isSensitive ? .red : .green)
.redacted(reason: isSensitive ? .placeholder: [])
}
}
}
// 1
private struct SensitiveKey: EnvironmentKey {
static let defaultValue: Bool = false
}
// 2
extension EnvironmentValues {
var isSensitive: Bool {
get { self[SensitiveKey.self] }
set { self[SensitiveKey.self] = newValue }
}
}
// 3
extension View {
func isSensitivePassword(_ value: Bool) -> some View {
environment(\.isSensitive, value)
}
}
When I try to make a custom environment value and read it, its not work, the key value not update at all.
You just need to inject into the environment
struct Custom_EnvironmentValues: View {
#State private var isSensitive = false
var body: some View {
VStack {
Toggle(isSensitive ? "Sensitive": "Not sensitive", isOn: $isSensitive)
PasswordField(password: "123456")
.isSensitivePassword(isSensitive) //your function name
}.padding()
}
}

How to listen to a computed property in SwiftUI?

I am trying to disable a button based on a computed property from the View Model, but is only disabled after the view is reloaded.
This is the View Model :
class VerifyFieldViewModel : ObservableObject {
#ObservedObject var coreDataViewModel = CoreDataViewModel()
func isValidFirstName() -> Bool {
guard coreDataViewModel.savedDetails.first?.firstName?.count ?? 0 > 0 else {
return false
}
return true
}
func isValidLastName() -> Bool {
guard coreDataViewModel.savedDetails.first?.lastName?.count ?? 0 > 0 else {
return false
}
return true
}
var isFirstNameValid : String {
if isValidFirstName() {
return ""
} else {
return "Name is empty"
}
}
var isLastNameValid : String {
if isValidLastName() {
return ""
} else {
return "Surname is empty"
}
}
var isSignUpComplete: Bool {
if !isValidFirstName() || !isValidLastName() {
return false
}
return true
}
}
This is how I am disabling the button .
struct CartsView: View {
#State var onboardingState: Int = 0
#StateObject var coreDataViewModel = CoreDataViewModel()
#ObservedObject var verifyFieldViewModel = VerifyFieldViewModel()
var body: some View {
ZStack {
switch onboardingState {
case 0 :
VStack {
detailOrder
.transition(transition)
Spacer()
bottomButton
.padding(30)
}
case 2 :
VStack {
detailOrder2
.transition(transition)
Spacer()
bottomButton
.padding(30)
.opacity(verifyFieldViewModel.isSignUpComplete ? 1 : 0.6)
.disabled(!verifyFieldViewModel.isSignUpComplete)
}
default:
EmptyView()
}
}
}
}
This is the Core Data View Model :
class CoreDataViewModel : ObservableObject {
let manager = CoreDataManager.instance
#Published var savedDetails : [Details] = []
init() {
fetchSavedDetails()
}
func fetchSavedDetails() {
let request = NSFetchRequest<Details>(entityName: "Details")
do {
savedDetails = try manager.context.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
func saveContext() {
DispatchQueue.main.async {
self.manager.save()
self.fetchSavedDetails()
}
}
}
NOTE : It works, but only when the view is reloaded.
EDITED : I updated the question to make it easier to understand. Hope that you can help me now.
EDITED2: Added Core Data View Model .
As mentioned above you don't need a computed property in this case. I made a small example of Login procedure which demonstrates the same behavior.
class LoginViewModel: ObservableObject {
#Published var username: String = ""
#Published var password: String = ""
var isValid: Bool {
(username.isNotEmpty && password.isNotEmpty)
}
func login() {
// perform login
}
}
struct ContentView: View {
#StateObject private var vm: LoginViewModel = LoginViewModel()
var body: some View {
Form {
TextField("User name", text: $vm.username)
TextField("Password", text: $vm.password)
Button("Login") {
vm.login()
}.disabled(!vm.isValid)
}
}
}

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

setting computed property in a SwiftUI view doesn't compile

Trying to set the computed property s in a SwiftUI view gets compiler error "Cannot assign to property: 'self' is immutable".
How do I have to I call the setter?
struct Test: View{
#State var _s = "test"
#State var _s2 = true
private var s : String
{ get { _s }
set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
var body: some View
{ Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
Aha... I see. Just use non mutating set
private var s : String
{ get { _s }
nonmutating set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
That is, why you already have #State property wrapper in your View.
struct Test: View{
#State var s = "test"
var body: some View {
Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
You able to change s directly from your code because s is wrapped with #State.
this is functional equivalent of the above
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
Text("\(s.wrappedValue)")
.onTapGesture {
self.s.wrappedValue = "beta"
}
}
}
}
Or if Binding is needed
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
TextField("label", text: s.projectedValue)
}
}
}

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