Issue with viewModel and TextField - swiftui

I'm not sure whether it's a SwiftUI bug or it's my fault:
When I type some text in a TextField and press the return button on my keyboard (in order to hide my keyboard), the typed text is removed and the TextField is empty again. I've tried this solution on different simulators and on a real device as well. The issue appears every time. I'm using iOS 14.3, Xcode 12.4
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: CreateNewCardViewModel())
})
}
}
#SwiftPunk: Here is my second question:
Let's say my view model has an additional parameter (id):
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
This parameter needs to be passed when I create the view to my viewModel. For this example let's say we iterate over some elements that have the id:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// is the same problem as before
// because now I create every time a new viewModel, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}

Your issue is here, that you did not create a StateObject in main View, and every time you pressed the key on keyboard you created a new model which it was empty as default!
import SwiftUI
struct ContentView: View {
#State var showNew = false
#StateObject var viewModel: CreateNewCardViewModel = CreateNewCardViewModel() // <<: Here
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: viewModel)
})
}
}
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}

Related

SwiftUI - Picker value not changing when accessing data from UserDefaults

I am making an app where I am showing different views based of user's selection by a picker. The binding value of the picker is initially set by UserDefaults in a viewModel. The problem is when I choose a picker value in my app, The picker automatically go back to initial state, as if someone forcing the picker not the change the values.
Settings ViewModel :
import Foundation
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
//Here is the problem
#Published var choosenUserType = UserDefaults.standard.string(forKey: "userType"){
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Home View:
import SwiftUI
struct HomeView: View {
#StateObject var settingsVM = SettingsViewModel()
var body: some View {
VStack{
switch settingsVM.choosenUserType{
case "Client":
Text("This is client")
case "Worker":
Text("This is worker")
default:
Text("This is default")
}
}.navigationTitle("Tanvirgeek Co")
.navigationBarItems(trailing: Button(action: {
settingsVM.showSettings.toggle()
}, label: {
Text("Settings")
}))
.sheet(isPresented: $settingsVM.showSettings, content: {
SettingsView(dissmiss: $settingsVM.showSettings)
.environmentObject(settingsVM)
})
}
}
Settings View:
import SwiftUI
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type"), content: {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text("\(userType)")
}
})
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}
What I am doing wrong? How to change the picker's binding variable value through the picked value here?
Your choosenUserType ends up with an inferred type of String? because that's what UserDefaults.string(forKey:) returns.
The Picker's selection type needs to match exactly with the tag type. The tags (which are inferred in this case as well) are of type String.
I've solved this by giving a default value to choosenUserType so that it can be a String (not String?):
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
#Published var choosenUserType : String = UserDefaults.standard.string(forKey: "userType") ?? SettingsViewModel.userTypes[0] {
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Also, in your SettingsView, you don't have to interpolate the userType in the Text -- you can just provide it directly:
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type")) {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text(userType)
}
}
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

How to swap my #State of my SwiftUI view for my view model #Published variable?

I have a button that triggers my view state. As I have now added a network call, I would like my view model to replace the #State with its #Publihed variable to perform the same changes.
How to use my #Published in the place of my #State variable?
So this is my SwiftUI view:
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
// This is the value I want to use as #Publisher
#State var isLoggedIn = false
var body: some View {
ZStack {
Button(action: {
// Before my #State was here
// self.isLoggedIn = true
self.viewModel.login()
}) {
Text("Log in")
}
if isLoggedIn {
TutorialView()
}
}
}
}
And this is my model:
final class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
private var subscriptions = Set<AnyCancellable>()
func demoLogin() {
AuthRequest.shared.login()
.sink(
receiveCompletion: { print($0) },
receiveValue: {
// My credentials
print("Login: \($0.login)\nToken: \($0.token)")
DispatchQueue.main.async {
// Once I am logged in, I want this
// value to change my view.
self.isLoggedIn = true } })
.store(in: &subscriptions)
}
}
Remove state and use view model member directly, as below
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
self.viewModel.demoLogin()
}) {
Text("Log in")
}
if viewModel.isLoggedIn { // << here !!
TutorialView()
}
}
}
}
Hey Roland I think that what you are looking for is this:
$viewMode.isLoggedIn
Adding the $ before the var will ensure that SwiftUI is aware of its value changes.
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
viewModel.login()
}) {
Text("Log in")
}
if $viewMode.isLoggedIn {
TutorialView()
}
}
}
}
class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
func login() {
isLoggedIn = true
}
}

How to add an observable property when other properties change

I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.

Problems with EnvironmentObject in ModalView

I have created a simple List and want to add users to it. My project has CoreDate activated and I have add the following Code to the SceneDelegate:
let userStorage = UserStorage()
let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(userStorage)
The code of the ContentView is this:
import SwiftUI
struct User: Identifiable {
var id = UUID()
var firstName = ""
var lastName = ""
}
class UserStorage: ObservableObject {
#Published var users = [User]()
}
struct ContentView: View {
#State private var presentation = false
#EnvironmentObject var userStorage: UserStorage
var body: some View {
VStack {
Button(action: {
self.presentation = true
}) {
Text("New User")
}.sheet(isPresented: $presentation, onDismiss: {
self.presentation = false
}) {
newuserView(presentation: self.$presentation, newUser: User())
}
List(userStorage.users) { singleUser in
VStack {
Text(singleUser.firstName)
Text(singleUser.lastName)
}
}
}
}
}
struct newuserView : View {
#Binding var presentation: Bool
#State var newUser: User
#EnvironmentObject var userStarage: UserStorage
var body: some View {
VStack {
TextField("Put in first name please", text:$newUser.firstName)
TextField("Put in last name please", text:$newUser.lastName)
Button(action: {
self.userStarage.users.append(self.newUser)
self.presentation = false
}) {
Text("Add new User")
}disabled(newUser.lastName.isEmpty || newUser.firstName.isEmpty)
}.padding(.horizontal)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When I run my project and want to add a user, I get the following error:
Thread 1: Fatal error: No ObservableObject of type UserStorage found.
A View.environmentObject(_:) for UserStorage may be missing as an ancestor of this view.
I have tried to do this with .sheet, but it doesn't work
Sheet creates different view hierarchy so .environmentObject is not injected in view to be shown in sheet by default - you have to do it manually
}.sheet(isPresented: $presentation, onDismiss: {
self.presentation = false
}) {
newuserView(presentation: self.$presentation, newUser: User())
.environmentObject(self.userStorage)
}