How to best pass data for form editing in swuiftui (while having that data available to parent view and other subviews)? - ios

I have a SwiftUI form in a MyFormSubView.swift where I have multiple #State variables representing individual fields like Text, etc. My issue is my parent view "ContentView.swift" also needs access to this information, and other subviews "OtherView.swift" also would benefit from access for display or editing. My current approach, is to change all the #State to #Binding, which creates a headache because some forms could have up to 20 fields with some optional... what is the best way to handle this? Is there a way to simply pass an object and have that be 'editable'?
Approaches:
(Current, problem approach) Have multiple individual variables declared as #State
in the ContentView.swift, and pass each individual variable into
MyFormSubView.swift with those variables having #Binding in front of
them that are mapped to swiftui elements to show up as 'placeholder
text' in textboxes, etc. This is bad as I have potentially up to 30 fields with some being optional.
(What I Think I Desire) Have identifiable model with all the
fields (and maybe pass this model into the MyFormSubView.swift, and
if it's possible, bind to it and just have it such that each field
is $mymodel.field1, $mymodel.field2, etc... which eliminates the
need to have 30+ variables passed into this thing.
(Maybe Better?) Use an #ObservableObject.
Is #2 possible? Or is there an even better way? Sample code would be great!

There are several ways to pass data like this across Views. Here is a quick implementation outlining 4 approaches.
You can use an #ObservableObject to reference a class with all of your data inside. The variables are #Published, which allows the View to update in the same way a #State variable would.
You can use an #StateObject. This is the same as #ObservableObject, except it will only initialize once and if the view re-renders the variable will persist (whereas an #ObservedObject would reinitialize). Read more about the difference here.
You can use an #EnvironmentObject. This is the same as #ObservedObject, except it is stored in the Environment, so you don't have to manually pass it between views. This is best when you have a complex view hierarchy and not every view needs a reference to the data.
You can create a custom Model and use a #State variable.
All of these methods work, but based on your description, I'd say the 2nd method is probably best for your situation.
class DataViewModel: ObservableObject {
#Published var text1: String = "One"
#Published var text2: String = "Two"
#Published var text3: String = "Three"
}
struct DataModel {
var text1: String = "Uno"
var text2: String = "Dos"
var text3: String = "Tres"
}
struct AppView: View {
var body: some View {
MainView()
.environmentObject(DataViewModel())
}
}
struct MainView: View {
#StateObject var dataStateViewModel = DataViewModel()
#ObservedObject var dataObservedViewModel = DataViewModel()
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#State var dataStateModel = DataModel()
#State var showSheet: Bool = false
#State var showOtherView: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
Button(action: {
showSheet.toggle()
}, label: {
Text("Button 1")
})
.sheet(isPresented: $showSheet, content: {
FormView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
.environmentObject(dataEnvironmentViewModel) // Sheet is a new environment
})
Button(action: {
showOtherView.toggle()
}, label: {
Text("Button 2")
})
if showOtherView {
ThirdView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
}
}
}
}
struct FormView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form(content: {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("BACK")
})
Text("EDIT TEXT FIELDS:")
TextField("Placeholder 1", text: $dataStateViewModel.text1)
.foregroundColor(.red)
TextField("Placeholder 2", text: $dataObservedViewModel.text2)
.foregroundColor(.blue)
TextField("Placeholder 3", text: $dataEnvironmentViewModel.text3)
.foregroundColor(.green)
TextField("Placeholder 4", text: $dataStateModel.text1)
.foregroundColor(.purple)
})
}
}
struct ThirdView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
}
}
}

Use ObservalbeObject, here is simple example how you can share data:
Step 1 - Add some kind of state:
class AppState: ObservableObject {
#Published var value: String = ""
}
Step 2 - Pass state to ContentView via setting enviromentObject
ContentView()
.environmentObject(AppState())
Step 3 - Now AppState will be available in all child views of ContentView, so here is the code for ContentView and OtherView. OtherView has the TextField, text from will be saved to AppState and you can be able to see it, when you press 'back' from OtherView.
ContentView:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
Text("Content View")
.padding()
NavigationLink(
destination: OtherView()
) {
Text("Open other view")
}
Text("Value from other view: \(appState.value)")
}
}
}
}
OtherView:
import SwiftUI
struct OtherView: View {
#EnvironmentObject var appState: AppState
#State var value: String = ""
var body: some View {
VStack {
Text("Other View")
.padding()
TextField("Enter value", text: Binding<String>(
get: { self.value },
set: {
self.value = $0
appState.value = $0
}
))
.frame(width: 200)
.padding()
}
}
}
This is just simple example, for more complex cases you can take a look on VIPER or MVVM patterns in Swift UI. For example, here:
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern
https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios

Related

Swiftui form re-using at multiple place not working

I have created model and using that model Im modify variable data at multiple places I can modify and enter data succesfully in FirstView. I could able to modify data in the SecondView. In SecondView, Whatever content I type in the textfield it goes away instanly (in short not allowing to enter data and ofc no error shown)
I want to know am i using proper object variable to call model every time
class MainViewModel: ObservableObject {
#Published var name = ""
#Published var age = ""
}
// Using at one place
struct FirstView : View {
#StateObject var mainViewModel = MainViewModel()
var body: some View {
Form {
TextField("", text: self.$MainViewModel.name)
TextField("", text: self.$MainViewModel.age)
}
}
}
// ReUsing same at another place
struct SecondView : View {
#EnvironmentObject var mainViewModel = MainViewModel()
var body: some View {
Form {
TextField("", text: self.$MainViewModel.name)
TextField("", text: self.$MainViewModel.age)
}
}
}
I have tried using #EnvironmentObject using at both view but doesnt work either here
Change
#EnvironmentObject var mainViewModel = MainViewModel()
To
#EnvironmentObject var mainViewModel : MainViewModel
Make sure you are injecting in the parent view
.environmentObject(mainViewModel)
#lorem ipsum explain the question perfectly. I am just converting his comments into working code. Please have look. This will make you more clear about your issue about injecting from parent.
import SwiftUI
#main
struct StackOverflowApp: App {
#State private var searchText = ""
var body: some Scene {
WindowGroup {
NavigationView {
FirstView()
.environmentObject(MainViewModel())
}
}
}
}
import SwiftUI
class MainViewModel: ObservableObject {
#Published var name = ""
#Published var age = ""
}
// Using at one place
struct FirstView : View {
#EnvironmentObject var mainViewModel : MainViewModel
var body: some View {
VStack {
Form {
TextField("", text: $mainViewModel.name)
TextField("", text: $mainViewModel.age)
}
NavigationLink {
SecondView()
.environmentObject(mainViewModel)
// Either you can inject new or same object from child to parent. #lorem ipsum
// .environmentObject(MainViewModel())
} label: {
Text("Second View")
}
}
}
}
// ReUsing same at another place
struct SecondView : View {
#EnvironmentObject var mainViewModel : MainViewModel
var body: some View {
Form {
TextField("", text: $mainViewModel.name)
TextField("", text: $mainViewModel.age)
}
}
}

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

SwiftUI: Why onReceive run duplicate when binding a field inside ObservableObject?

This is my code and "print("run to onReceive (text)")" run twice when text change (like a image). Why? and thank you!
import SwiftUI
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
.onReceive(viewModel.$text) { text in
print("run to onReceive \(text)")
}
}
}
I think it's because the view is automatically updated as your #Published property in your ViewModel changes and the .onReceive modifier updates the view yet again due to the 2 way binding created by viewModel.$text resulting in the view being updated twice each time.
If you want to print the text as it changes you can use the .onChange modifier instead.
class ContentViewViewModel: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}.onChange(of: viewModel.text) { newValue in
print("run to onChange \(newValue)")
}
}
}
onChanged in SwiftUI
Because you have a #Published variable inside the #StateObject of your view, the changes in that variable will automatically update the view.
If you add the .onReceive() method, you will:
update the view because you have the #Published var
update it again when the .onReceive() method listens to the change
Just delete the .onReceive() completely and it will work:
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
// It still works without this modifier
//.onReceive(viewModel.$text) { text in
// print("run to onReceive \(text)")
//}
}
}

How to pass an Object to new SwiftUI view

I'm having trouble getting started in SwiftUI. What I want to do is rather simple at least I thought it would. What I want to do is that the ContentView expects a first and last name of a person. The button "Add to list" adds the person to a list and the second button shows a list of all added persons in a second view. I read about the property wrappers but I cannot get it to work. Do I need to change struct Person to a class in order to use the #ObservedObject for initialising #StateObject var listOfPersons = [Person]() or is there a more simpler Swift like way to pass the list to my PersonList View?
My project code:
ContentView.swift
struct ContentView: View {
#State var firstName: String = ""
#State var lastName: String = ""
#StateObject var listOfPersons = [Person]()
var body: some View {
NavigationView {
ZStack (alignment: .top){
Color(.orange).opacity(0.2).edgesIgnoringSafeArea(.all)
VStack {
Text("Hello stranger")
.font(.title)
TextField("Fist name:", text: $firstName)
.padding()
TextField("Last name:", text: $firstName)
.padding()
HStack(spacing: 40) {
Button("Add to list") {
listOfPersons.append(Person(firstName: firstName, lastName: lastName))
}
.padding()
NavigationLink(destination: PersonList()) {
Text("Show list")
}
...
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PersonList.swift
import SwiftUI
struct PersonList: View {
#Binding var listOfPersons = [Person]()
var body: some View {
Color(.orange).opacity(0.2).edgesIgnoringSafeArea(.all)
List(listOfPersons) { person in
PersonRow(person: person) }
}
}
struct PersonList_Previews: PreviewProvider {
static var previews: some View {
PersonList()
}
}
Person.swift
import Foundation
struct Person {
var firstName: String
var lastName: String
}
PersonRow.swift
import SwiftUI
struct PersonRow: View {
var person: Person
var body: some View {
Text("\(person.firstName), \(person.lastName)")
}
}
Your code has a couple problems, but you're on the right track. First, replace #StateObject var listOfPersons = [Person]() with:
#State var listOfPersons = [Person]()
#StateObject is for an instance of a ObservableObject class. #State is what you should be using for a simple array of the Person struct.
Then, in your current code, you're just instantiating a plain PersonList without any parameters. You want to pass in listOfPersons.
/// here!
NavigationLink(destination: PersonList(listOfPersons: $listOfPersons)) {
Text("Show list")
}
The $ sign gets the underlying Binding<[Person]> from listOfPersons, which means that any changes made inside PersonList's listOfPersons will be reflected back to ContentView's listOfPersons.
Finally, in PersonList, change #Binding var listOfPersons = [Person]() to
struct PersonList: View {
#Binding var listOfPersons: [Person]
...
}
#Binding almost never needs to have a default value, since it's always passed in.

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

Resources