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.
Related
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)
}
}
}
In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.
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 😄
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
I am struggling with figuring out how to use a value assigned to a variable in an ObservableObject class in another view on another sheet. I see that it gets updated, but when I access it in the new view on the new sheet it is reset to the initialized value. How do I get it to retain the new value so I can use it in a new view on a new sheet?
ContentData.swift
import SwiftUI
import Combine
class ContentData: ObservableObject {
#Published var text: String = "Yes"
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var contentData = ContentData()
#State private var inputText: String = ""
#State private var showNewView: Bool = false
var body: some View {
VStack {
TextField("Text", text: $inputText, onCommit: {
self.assignText()
})
Button(action: {
self.showNewView = true
}) {
Text("Go To New View")
}
.sheet(isPresented: $showNewView) {
NewView(contentData: ContentData())
}
}
}
func assignText() {
print(contentData.text)
contentData.text = inputText
print(contentData.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(contentData: ContentData())
}
}
NewView.swift
import SwiftUI
struct NewView: View {
#ObservedObject var contentData = ContentData()
var body: some View {
VStack {
Text(contentData.text)
}
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(contentData: ContentData())
}
}
I have tried many, many different methods I have seen from other examples. I tried doing it with #EnviromentObject but could not get that to work either. I also tried a different version of the NewView.swift where I initialized the value with:
init(contentData: ContentData) {
self.contentData = contentData
self._newText = State<String>(initialValue: contentData.text)
}
I think I am close, but I do not see what I am missing. Any help would be very much appreciated. Thanks.
#ObservedObject var contentData = ContentData()
ContentData() in the above line creates a new instance of the class ContentData.
You should pass the same instance from ContentView to NewView to retain the values. Like,
.sheet(isPresented: $showNewView) {
NewView(contentData: self.contentData)
}
Stop creating new instance of ContentData in NewView and add the ability to inject ContentData from outside,
struct NewView: View {
#ObservedObject var contentData: ContentData
...
}