How to listen to a computed property in SwiftUI? - swift

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

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

NavigationLink In NavigationStack is disabled

I wrote test code for NavigationStack. The behavior of the code is a two-step transition(ContentView -> SubsubTestView -> DetailView).
But I got an error when I have selected a name in SubsubTestView.
A NavigationLink is presenting a value of type “User” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Is there anything wrong with the wrong part of the code?
ContentView.swift
import SwiftUI
class EnvObj: ObservableObject {
#Published var users = [User(name: "a"), User(name: "b"), User(name: "c")]
}
struct User: Hashable, Identifiable, Equatable {
var id = UUID()
var name = ""
static func == (lhs: User, rhs: User) -> Bool{
return lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var envObj: EnvObj
#State var moveToSubsub = false
var body: some View {
NavigationStack {
Button("To subsub") {
moveToSubsub = true
}
.navigationDestination(isPresented: $moveToSubsub) {
SubsubTestView(vm: VM(envObj: envObj))
}
}
}
}
struct SubsubTestView: View {
#EnvironmentObject var envObj: EnvObj
#StateObject var vm: VM
var body: some View {
VStack {
List(self.vm.envObj.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationDestination(for: User.self) { user in
DetailView(vm: VMD(envObj: envObj, selectedUser: user))
}
}
}
}
class VM: ObservableObject {
var envObj: EnvObj = .init()
init(envObj: EnvObj) {
self.envObj = envObj
}
}
struct DetailView: View {
#StateObject var vm: VMD
var body: some View {
VStack {
TextField("Name: ", text: (self.$vm.selectedUser ?? User()).name)
Text(self.vm.selectedUser?.name ?? User().name)
Button("submit", action: self.vm.submit)
}
}
}
class VMD: ObservableObject {
var envObj: EnvObj = .init()
#Published var selectedUser: User?
init(envObj: EnvObj, selectedUser: User? = nil) {
self.envObj = envObj
self.selectedUser = selectedUser
}
private(set) lazy var submit = {
if let user = self.selectedUser {
self.update(user: user)
}
}
func update(user: User) {
self.envObj.users = self.envObj.users.map {
return $0 == user ? user : $0
}
}
}
func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
return Binding(get: {
binding.wrappedValue ?? fallback
}, set: {
binding.wrappedValue = $0
})
}
Thanks,

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

SwiftUI: Binding to #AppStorage

In the following example how can I change the value of activeSheet based on how SwiftUI updates aArrived and bArrived?
struct ContentView: View {
#AppStorage("didAArrive") var aArrived: Bool = false
#AppStorage("didBArrive") var bArrived: Bool = false
enum ActiveSheet: Identifiable {
case aArrived, bArrived
var id: Int {
hashValue
}
}
#State private var activeSheet: ActiveSheet?
var body: some View {
Text("Hello")
.sheet(
item: $activeSheet,
content: { item in
switch item {
case .aArrived:
Text("A arrived")
case .bArrived:
Text("B arrived")
}
}
)
}
}
You can create a custom binding for the sheet which gets its value based on aArrived and bArrived. The binding value will be initialised based on aArrived or bArrived and get updated every time that either one changes.
struct ContentView: View {
#AppStorage("didAArrive") var aArrived: Bool = false
#AppStorage("didBArrive") var bArrived: Bool = false
enum ActiveSheet: Identifiable {
case aArrived, bArrived
var id: Int {
hashValue
}
}
var body: some View {
let sheetBinding = Binding<ActiveSheet?>(
get: {
if aArrived && bArrived {
return ActiveSheet.aArrived
} else if aArrived {
return ActiveSheet.aArrived
} else if bArrived {
return ActiveSheet.bArrived
} else {
return nil
}
},
set: { _ in }
)
VStack(spacing: 20) {
Toggle("A arrived", isOn: $aArrived)
Toggle("B arrived", isOn: $bArrived)
}
.sheet(
item: sheetBinding,
content: { item in
switch item {
case .aArrived:
Text("A arrived")
case .bArrived:
Text("B arrived")
}
}
)
}
}

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}