I have a View and a ViewModel and would like to set some variables in the ViewModel and read AND set them in the View. In the ViewModel I have a #Published variable. When I try to modify the value of the variable in the View, the errors I get are: 'Cannot assign to property: '$testModel' is immutable' and 'Cannot assign value of type 'String' to type 'Binding'. Is there a way to do this? The code I have now is as follows:
View
struct TestView: View {
#ObservedObject var testModel: TestViewModel = TestViewModel()
var body: some View {
VStack(alignment: .center, spacing: 4) {
Button(action: {
$testModel.test = "test2"
}) {
Text("[ Set value ]")
}
}
}
}
ViewModel
class TestViewModel: ObservableObject {
#Published var test = "test"
}
Since you don't need the Binding to set the value, you don't need $testModel, just testModel.
Change code to this:
Button(action: {
testModel.test = "test2"
}) {
Text("[ Set value ]")
}
It's also worth noting that you should use #StateObject rather than #ObservedObject here to prevent TestViewModel potentially being initialized multiple times.
Related
I have one view (with a Form), a viewModel, and a second view that I hope to display inputs in the Form of the first view. I thought property wrapping birthdate with #Published in the viewModel would pull the Form input, but so far I can't get the second view to read the birthdate user selects in the Form of the first view.
Here is my code for my first view:
struct ProfileFormView: View {
#EnvironmentObject var appViewModel: AppViewModel
#State var birthdate = Date()
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {
DatePicker("Birthdate", selection: $birthdate, displayedComponents: .date)
}
}
}
Here is my viewModel code:
class AppViewModel: ObservableObject {
#Published var birthdate = Date()
func calcAge(birthdate: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let birthdayDate = dateFormater.date(from: birthdate)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
let age = calcAge.year
return age!
and here is my second view code:
struct UserDataView: View {
#EnvironmentObject var viewModel: AppViewModel
#StateObject var vm = AppViewModel()
var body: some View {
VStack {
Text("\(vm.birthdate)")
Text("You are signed in")
Button(action: {
viewModel.signOut()
}, label: {
Text("Sign Out")
.frame(width: 200, height: 50)
.foregroundColor(Color.blue)
})
}
}
And it may not matter, but here is my contentView where I can tab between the two views:
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
ZStack {
if viewModel.signedIn {
ZStack {
Color.blue.ignoresSafeArea()
.navigationBarHidden(true)
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
UserDataView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
}
}
}
else
{
SignInView()
}
}
}
.onAppear {
viewModel.signedIn = viewModel.isSignedIn
}
}
One last note, I've got a second project that requires this functionality (view to viewmodel to view) so skipping the viewmodel and going direct from view to view will not help.
Thank you so much!!
Using a class AppViewModel: ObservableObject like you do is the appropriate way to "pass" the data around your app views. However, there are a few glitches in your code.
In your first view (ProfileFormView), remove #State var birthdate = Date() and use
DatePicker("Birthdate", selection: $appViewModel.birthdate, ....
Also remove #StateObject var vm = AppViewModel() in your second view (UserDataView),
you already have a #EnvironmentObject var viewModel: AppViewModel, no need for 2 of them.
Put #StateObject var vm = AppViewModel() up in your hierarchy of views,
and pass it down (as you do) using the #EnvironmentObject with
.environmentObject(vm)
Read this info to understand how to manage your data: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
I want to show a flag in the main view if the user selected that country 2 views before getting to the main view.
The code for the page where the user selects the country is:
struct ChooseLanguageWithButtonView: View {
#State var isSelected1 = false
#State var isSelected2 = false
var body: some View {
ZStack {
Color.white
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView (showsIndicators: false) {
VStack(spacing: 20){
VStack(alignment: .leading){
Text("Choose the language you want to learn!")
.font(.custom("Poppins-Bold", size: 25))
Text("Select one language")
.opacity(0.5)
.font(.custom("Poppins-Light", size: 15))
.frame(alignment: .bottomTrailing)
.padding(5)
Spacer()
}
.padding(.top, -25)
HStack (spacing: 30){
ButtonLanguage(
isSelected: $isSelected1,
color: .orangeGradient1,
textTitle: "English",
textSubtitle: "American",
imageLang: "englishAmerican",
imageFlag: "1"
)
.onTapGesture(perform: {
isSelected1.toggle()
if isSelected1 {
isSelected2 = false
}
})
ButtonLanguage(
isSelected: $isSelected2,
color: .orangeGradient1,
textTitle: "English",
textSubtitle: "British",
imageLang: "telephone",
imageFlag: "0"
)
.onTapGesture(perform: {
isSelected2.toggle()
if isSelected2 {
isSelected1 = false
}
})
}
NavigationLink(destination: {
if isSelected1 {
EnglishAmericanLevelView()
} else if isSelected2 {
EnglishBritishView()
}
}, label: {
Text("Let's Go")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50)
.cornerRadius(40)
})
On the main view if the user chose english american I want to show the american flag.
Someone can help me with that please?
Assuming you are using MVVM pattern, then you can create a published Bool variable in the ViewModel and pass it in the environment.
Something passed in the environment can be accessed from all descendents of the view.
class MainViewModel: ObservableObject {
#Published var showAmericanFlag: Bool = false
}
Pass the ViewModel it to either the MainView or ChildView as an environment object.
// Wherever you are using MainView
MainView()
.environmentObject(MainViewModel())
// or create MainViewModel inside MainView and pass it to ChildView
struct MainView: View {
private var mainViewModel = MainViewModel()
var body: some View {
// stuff
ChildView()
.environmentObject(mainViewModel())
// stuff
}
}
You can get a reference to the MainViewModel inside the ChildView from the
environment and use the showAmericanFlag variable.
struct ChildView: View {
#EnvironmentObject private var mainViewModel: MainViewModel
var body: some View {
// stuff
if mainViewModel.showAmericanFlag {
Image("americanFlag")
}
// stuff
}
}
It sounds like a global class might be good to use here, so you can set variables like this that later you can reference in your views.
final class GlobalClass: ObservableObject {
#Published public var showFlag: Bool = false
}
In your main project app file you can initialize the class with the .environmentObject method
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(GlobalClass())
}
}
}
You can then reference the global class in any view as follows
#EnvironmentObject private var globalObj: GlobalClass
Then you can set the variable to be whatever you'd like and then use it in an if statement to show your image. For example . . .
if (globalObj.showFlag){
Image("flag").onTapGesture{
globalObj.showFlag = false
}
}
Otherwise you will have to pass the show flag object from view to view
I am just learning to code and I have a question. How do I store the Input data of a Textfield and display it in another View? I tried it with Binding but it doesn't work that way. I appreciate your help
import SwiftUI
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
// the name should be diplayed here!
Text(name)
}
}
struct DisplayUserName_Previews: PreviewProvider {
static var previews: some View {
DisplayUserName()
}
}
State should always be stored in a parent and passed down to the children. Right now, you're not showing the connection between the two views (neither reference the other), so it's a little unclear how they relate, but there are basically two scenarios:
Your current code would work if DisplayUserName was the parent of SelectUserName:
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
Text(name)
SelectUserName(name: $name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
Or, if they are sibling views, the state should be stored by a common parent:
struct ContentView : View {
#State private var name = ""
var body: some View {
SelectUserName(name: $name)
DisplayUserName(name: name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
var name : String //<-- Note that #State isn't needed here because nothing in this view modifies the value
var body: some View {
Text(name)
}
}
I'm back again lol. My content view looks like:
struct ContentView: View {
#ObservedObject var VModel = ViewModel()
#State private var resultsNeedToBeUpdated: Bool = false
var body: some View {
VStack {
if self.resultsNeedToBeUpdated == true {
SearchResults(VModel: VModel, resultsNeedToBeUpdated: $resultsNeedToBeUpdated)
}
}
}
}
The SearchBar view looks like:
struct SearchResults: View {
var VModel: ViewModel
#Binding var resultsNeedToBeUpdated: Bool
var body: some View {
List {
ForEach(VModel.searchResults, id: \.self) { result in
Text(result)
}
}
}
}
Finally, the ViewModel class looks like:
class ViewModel: ObservableObject {
#Published var searchResults: [String] = []
func findResults(address: String) {
let Geocoder = Geocoder(accessToken: 'my access token')
searchResults = []
Geocoder.geocode(ForwardGeocodeOptions(query: address)) { (placemarks, attribution, error) in
guard let placemarks = placemarks
else {
return
}
for placemark in placemarks {
self.searchResults.append(placemark.formattedName)
print("Formatted name is: \(placemark.formattedName)") //this works
}
}
//I'm doing my printing on this line and it's just printing an empty array ;(
}
The variable 'resultsNeedToBeUpdated' is a boolean Binding that is updated when the user types some text into a search bar view, and it essentially just tells you that the SearchResults view should be displayed if it's true and it shouldn't be displayed if it's false. What I'm trying to do is update the SearchResults view depending on what the user has typed in.
The error is definitely something with the display of the SearchResults view (I think it's only displaying the initial view, before the array is updated). I tried using a binding because I thought it would cause the ContentView to reload and it would update the SearchResultsView but that didn't work.
Make view model observed, in this case it will update view every time the used #Published var searchResults property is changed
struct SearchResults: View {
#ObservedObject var VModel: ViewModel
// #Binding var resultsNeedToBeUpdated: Bool // << not needed here
additionally to above published properties should be updated on main queue, as
DispatchQueue.main.async {
self.searchResults = placemarks.compactMap{ $0.formattedName }
// for placemark in placemarks {
// print("Formatted name is: \(placemark.formattedName)") //this works
// }
}
I have an array of SplitItem objects I am trying to do a for loop on them and show a textfield. I keep getting the error Use of unresolved identifier '$item' for this code,
import SwiftUI
struct ContentView: View {
#State var selectedAccount:Int = -1
#State var splitItems:[SplitItem] = [
SplitItem(account: 0, amount: "1.00", ledger: .Accounts),
SplitItem(account: 0, amount: "2.00", ledger: .Accounts),
SplitItem(account: 0, amount: "3.00", ledger: .Budgets),
SplitItem(account: 0, amount: "4.00", ledger: .Budgets)
]
var body: some View {
VStack {
ForEach(self.splitItems) { item in
TextField(item.amount, text: $item.amount)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SplitItem: Identifiable {
#State var id:UUID = UUID()
#State var account:Int
#State var amount:String
#State var ledger:LedgerType
}
enum LedgerType:Int {case Accounts=0,Budgets=1}
if I change the text: $item.amount to text: item.$amount it compiles but the resulting textfield does not let me change it. The same thing if I change the for loop to indices and try to bind based on the index,
ForEach(self.splitItems.indices) { index in
TextField(self.splitItems[index].amount, text: self.$splitItems[index].amount)
}
it has no problem showing a Text(item.amount) its only when I try to bind do I have a problem. I think it has something to do with it being an array because if I try to bind a single splititem not in an array to a textfield it works just fine.
edit I also tried making a subview with the textfield and calling that from the foreach loop but I got the same error.
also this is swiftui for Mac not iOS.
It is not Mac nor iOS specific.
First of all, use the state as the single source of truth for a given view only.
In your case, move your data and business logic to your model, represented by some ObservableObject. In your View use #ObservedObject property wrapper.
Your (simplified) data structure and model could be defined as
struct Item: Identifiable {
let id = UUID()
var txt = ""
}
class Model: ObservableObject {
#Published var items = [Item(txt: "alfa"), Item(txt: "beta")]
}
Now you are ready tu use this model in View. When the state value changes, the view invalidates its appearance and recomputes the body, and the same is true for ObservedObject.
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
ForEach(model.items.indices) { idx in
VStack {
Text(self.model.items[idx].txt.capitalized).bold()
TextField("label", text: self.$model.items[idx].txt).frame(idealWidth: 200)
}
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}