UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Passing Data Between Views - Modifer Nightmare

Forums > SwiftUI

How do I capture data from a TextField and essentially "store it" for display in another view?

And I am still getting errors and more confused than ever!

FILE ONE CALLED QuizSettings.swift

import Foundation
import SwiftUI

class QuizSettings: ObservableObject {

    @Published var groupName: String = ""
    //ideally, I'll be adding color and total here, but for now, just want to capture a "title" or "groupName"

}

FILE TWO CALLED WelcomeView (this is where we capture user input via the text field)

import SwiftUI
struct WelcomeView: View {

    @StateObject var myUserInput = QuizSettings()

    var body: some View {

              //Logo and instructional text in a VStack (not shown)

                TextField("Name", text: $myUserInput.groupName)
                                    .padding()
                                    .frame(width: 320, alignment: .center)
                                    .background(Color.white.opacity(0.95))
                                    .cornerRadius(10)
                                    .foregroundColor(.black)
                                    .font(.headline)
                                    .autocapitalization(UITextAutocapitalizationType.words)
                                    .shadow(color: $bkgdColor.wrappedValue, radius: 5, x: 0.0, y: 0.0)
                                    .focused($nameIsFocused)
                //                    .environmentObject(myUserInput) //As I try to solve this riddle via trial and error I comment/uncomment this line as needed. . . 

                //A Button and some static footer elements (not shown)
   }
 }

FILE THREE CALLED HeaderView (this is where I want whatever the user entered on WelcomeView, to be displayed)

  • NOTE: This is an "inner view," -- a file just to create the "header" of my app. I have a larger ContentView() file (not shown) that has HeaderView() in a VStack. . . not sure if that matters?
import SwiftUI

struct HeaderView: View {

    @EnvironmentObject var userInput: QuizSettings

    var body: some View {

        VStack (alignment: .leading) {
                Text("The group you entered is called:")
                    .font(.headline)
                    .foregroundColor(.white)

            Text(userInput.groupName)  //!! THIS IS WHERE THE ERROR IS THROWING!
                    .font(.largeTitle)
                    .bold()
                    .foregroundColor(.white)
                    .environmentObject(userInput)
            }
        }

}

Also keep in mind each of the 3 files above have a "Preview" section that ALSO has to be free of errors. (Which also need their own State/Observed/Object variables??) If you know how to fix my issue, please include "the fix" for these Preview sections too?

The specific error thrown is about not finding the EnvironmentObject Ancestor View. . .

Thread 1: Fatal error: No ObservableObject of type QuizSettings found. A View.environmentObject(_:) for QuizSettings may be missing as an ancestor of this view.

2      

Create a new XCode application for iOS. Paste this code. Study each comment. Please come back here and tell us what you learned about Observed and Observable objects.

import SwiftUI
// Sample code for jjohnson2022.
// By: Obelix, 17 Jan 2022.

// This is an object that any struct can watch for changes.
// You can OBSERVE this object for changes.
class Wizard: ObservableObject {
    @Published var wizardName: String = "Harry Potter" // This might change.
    // You can change the spell count.
    // But objects won't respond, because the value isn't published.
    var numberOfSpellsInEachEpisode = 42
}

struct WelcomeView: View {
    // Here create TWO unique versions of the object for fun. Two DIFFERENT Wizard objects.
    @StateObject var firstWizard  = Wizard()
    @StateObject var secondWizard = Wizard()  // Create a second one!
    var body: some View {
        VStack {
            Spacer()
            // Allow user to change the first wizard's name.
            TextField("Name", text: $firstWizard.wizardName)
                .padding()
                .background(Color.teal)
            Button  {
                secondWizard.numberOfSpellsInEachEpisode += 1 // Add one
                // Watch the console! The spell count goes up.
                // But the number is NOT being published!
                print("spell count is: \(secondWizard.numberOfSpellsInEachEpisode)")
            } label: {
                Label {
                    Text("Increase Spell Count")
                } icon: {
                    Image(systemName: "plus.circle")
                }
            }
            Spacer()
            Text("Notice which wizard changes when you update the name above.").padding()
            WizardView(student: firstWizard).padding(.bottom)  // First Wizard
            WizardView(student: secondWizard) // Second view gets a completely DIFFERENT wizard
            Spacer()
        }
        .onAppear {
            secondWizard.wizardName = "Hermione" // change it up.
        }
    }
}

// This is just a lego brick. Put this anywhere you need it.
// BUT you have to give it a Wizard to use it!
struct WizardView: View {
    @ObservedObject var student: Wizard  // You'll be OBSERVING the object for changes.
    var body: some View {
        VStack  {
            Text("The wizard is: \(student.wizardName)")// You'll redraw this view whenever this changes!
            Text("Spell count: \(student.numberOfSpellsInEachEpisode)") // Why does this not change?
        }
    }
}

2      

This is another example:

import SwiftUI

class Whatever: ObservableObject {
    @Published var someText: String
    init(someText: String) {
        self.someText = someText
    }
}

struct TotallyNewView: View {
    @ObservedObject var blueFish: Whatever
    var body: some View {
        Text(blueFish.someText)
    }
}

struct ContentView: View {
    @StateObject var pinkFish = Whatever(someText: "")
    var body: some View {
        NavigationView {
            Form{
                TextField("Enter your text", text: $pinkFish.someText)
                NavigationLink("Submit") {
                    TotallyNewView(blueFish: pinkFish)
                }
            }
        }
    }
}

You don't need previews. You can leave them out, comment them out, or delete them entirely. However, here are the previews for my code:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TotallyNewView_Previews: PreviewProvider {
    static var previews: some View {
        TotallyNewView(blueFish: Whatever(someText: "Any text here"))
    }
}

2      

Hello! I am here to follow-up as requested with "what I've learned."

  1. There are still nice, giving, and patient people in this world. THANK YOU @Obelix!!!! Thank you for everything. I am floored by your response.
  2. The spell count in WizardView does not get updated because it isn't @Published in the Wizard class --But I've learned oh, so much more. . .
  3. The entire class (Wizard) is an Observable Object. The "var"(s) inside are not the object(s). The ENTIRE CLASS is the Observed "Object."
  4. Describing the WizardView as a LEGO Brick was the perfect phrase. I see now that the View merely has a var called student. Again, the @ObserveredObject indicated in WizardView is the ENTIRE Wizard Class! (My mistake was again thinking the @ObservedObject is student. NO! It is the Wizard Class (merely "held" or "referenced" by the student variable) that is being observed.
  5. The Wizard View "taps" into the Wizard Class with normal dot syntax. But we use the SAME "student variable" to tap into the DIFFERENT class components (name and score). The @Published name will update. The score, being un-published, will not, even though it is "connected" it to the Wizard Class.
  6. In our main "Welcome View", you brilliantly created TWO @StateObjects "of type Wizard"
  7. What's interesting here is that the print statement will "+1" to the 2nd Wizard's score even though it is not @Published; but in a simulator, the View will NOT update (for either of our two Wizards) because it is not @Published.
  8. Finally, when we create our two WizardViews (showing name and spell count for each wizard), we pass our firstWizard and secondWizard @StateObjects as arguments to our student parameter. It is the same VIEW getting two different @StateObjects
  9. as a cherry on top, the .onAppear will "override" the default wizardName of "Harry Potter" and instead use "Hermionie" (because each @State"Wizard"Object get's it's very own WizardView).

The only other note to add for someone else following these footsteps, know that to really undestand this example, I put each View in it's own swift file. The "default code" for each file's preview worked fine EXCEPT for the WizardView file. To clear these errors, I had to pass Wizard.init() to the student parameter as follows:

struct WizardView_Previews: PreviewProvider {

    static var previews: some View {
        WizardView(student: Wizard.init())
    }
}

Again, all the credit due to @Obelix! I will now try to take these lessons into my original project :)

2      

I enjoyed Vince's addition!

He also showed how you can pass an Observed object to a view struct, but the view struct can appear in a different screen. (Via Navigation views.) This moves more towards your original question.

If you think of the lego brick, I just put two of them on the same screen as the TextField.

But you can create a Lego brick on a second screen and navigate TO that screen. But the same rules apply! You can't create the lego brick anywhere, unless you provide an appropriate object for that brick to display.

Glad this helped!

3      

In this post Scaffolding I describe setting up fake data for a PreviewProvider.

You craft a lego block, such as WizardView_Previews, to display just a small part of your entire application. It stands by itself. This preview is NOT connected to any other part of your application. This means to test your WizardView in preview mode, you must provide fake data.

You did this nicely with the Wizard.init() function. You created a new Wizard object and passed it in. Luckily, the Wizard object initializes all of its internal structures with default values (Harry Potter, and 42). You won't always be so lucky!

In that case, you may have to create a fake object with a proper initializer before you can pass it to your PreviewProvider struct.

struct WizardView_Previews: PreviewProvider {
    let previewWizard = Wizard()  // create a fake object for this preview.
    previewWizard.name = "Luna" // change it up.
    previewWizard.numberOfSpellsInEachEpisode = 5 // I did not actually count Luna's spells.
    static var previews: some View {
        WizardView(student: previewWizard) // pass in your fake object.
    }
}

This gets trickier when you want your preview to display an array of objects from a CoreData database. Or from a live json feed! In those cases, you may want to consider NOT using previews. If you're a glutton, others have posted deep dive solutions.
But one other thing to mention. Paul notes that this is so common, he almost always adds a static example object to his classes. In fact, he notes that always calls it "example". See: Static Properties May '22

So to follow his excellent guidance, we might define the Wizard class like this.

class Wizard: ObservableObject {
    @Published var wizardName: String = "Harry Potter" // This might change.
    // You can change the spell count.
    // But objects won't respond, because the value isn't published.
    var numberOfSpellsInEachEpisode = 42

    static let example = Wizard()  // create a Wizard with default values. To be used in Previews!
}

Then you can simplify the preview code like this:

struct WizardView_Previews: PreviewProvider {
    static var previews: some View {
        WizardView(student: Wizard.example // pass in your static object just for show and tell.
    }
}

If you ALWAYS use example to name your static class variable, you'll find it easy to update previews in future projects.

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.