How do I store the Input of a Textfield and display it in another View in Swift UI? - xcode

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

Related

SwiftUI, How to publish data from view to a viewModel then to a second view?

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

Populating SwiftUI List with array elements that can be editied in TextEditor

I have a SwiftUI app that produces a List made from elements of an array of columns held in a struct.
I need the items in the row to be editable so I'm trying to use TextEditor but the bindings are proving difficult. I have a working prototype however the TextEditors are uneditable - I get the warning:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Here's a much shortened version of my code which produces the same problem:
import SwiftUI
struct Item: Identifiable {
#State var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach (Array(wrapperArray.items.enumerated()), id: \.offset) { index, item in
TextEditor(text: item.$stringValue)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I bind the TextEditor to the items stringValues within the items array?
TIA.
#State should only be used as a property wrapper on your View -- not on your model.
You can use a binding within ForEach using the $ syntax to get an editable version of the item.
struct Item: Identifiable {
var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($wrapperArray.items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}
This could be simplified further to avoid the ArrayContainer if you want:
struct ContentView: View {
#State var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}

Dynamically updating a list in SwiftUI?

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

SwiftUI Text field default value

I am trying to set a default value in a text field but I don't know how.
//TextField("", text:$name)
struct MyView: View {
#State var name:String = ""
var body: some View {
TextField("", text:$name)
}
init(n:String) {
name = n
}
}
If I call MyView("Jack"), Jack does not appear as a value in the textfield.
This can be done in a following way
struct MyView: View {
#State var name: String
var body: some View {
TextField("", text:$name)
}
init(n: String) {
_name = State(initialValue: n)
}
}

Updating SwiftUI View based on Observable in Preview

Trying to implement a Login screen in SwiftUI. Based on other similar questions, I'm going the approach of using an Observable EnvironmentObject and a ViewBuilder in the main ContentView that reacts to that and displays the appropriate screen.
However, even though the property is updating as expecting the view never changes in Preview. Everything works fine when built and run in the Simulator but in Preview the change never happens.
Below is the code reduced to the smallest possible example in a single file (only missing passing the environment object in SceneDelegate, which doesn't affect Preview anyway).
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
#ViewBuilder
var body: some View {
if !userAuth.person.isLoggedin {
FirstView()
} else {
SecondView()
} }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct Person {
var isLoggedin: Bool
init() {
self.isLoggedin = false
}
}
class UserAuth: ObservableObject {
#Published var person: Person
init(){
self.person = Person()
}
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
willChange.send(self)
self.person.isLoggedin = true
didChange.send(self)
}
}
struct SecondView: View {
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
struct FirstView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack {
Button(action: {
self.userAuth.login()
}) {
Text("Login")
}
Text("Logged in: " + String(self.userAuth.person.isLoggedin))
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
EDIT: Based on the answer below, I've added the environment object to the interior views, but unfortunately the view still doesn't change in Preview mode.
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
environment object must be set in PreviewProvider as well
UPDATE
struct ContentView: View {
#ObservedObject var userAuth = UserAuth () // #ObservedObject
var body: some View {
NavigationView{ // Navigation
}.environmentObject(UserAuth) //.environmentObject
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct SecondView: View {
#EnvironmentObject var userAuth: UserAuth // only EnvironmentObject
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
The issue I was having with Canvas not giving previews is that my ObservableObject was reading from User Defaults
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") !
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
So works in simulator and on device but no Canvas Previews. I tried many ways to give Preview data to use since Preview can't read from UserDefaults (not a device), and realized I can put an initial/ default value if the UserDefault is not there:
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") ?? "Sean"
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
Now Preview/ Canvas is showing my view and I can continue coding with my Observable Object. The aim was to put in the Observable Object some default code to use.

Resources