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

Partially Solved: Persisting TextField (Issue With View Updating)

Forums > SwiftUI

Hey All,

So I posted this a bit ago and took it down shortly after because I thought I had a solution to my problem. My solution wasn't a solution and has now sent me down a much larger rabbit hole than I started in.

So, I have run into an issue where a piece of my UI doesn't update. I traced it back to the @AppStorage wrapper not being observed by SwiftUI. Effectively, I have two properties that store the exact same thing. One of them is simply used as the binding for the TextField and the other holds the converted double that is used in a conditional view.

@AppStorage("TotalHours") var totalHours: String = ""
@AppStorage("DoubleTotalHours") var doubleTotalHours: Double = 0.0

But, since @AppStorage isn't observed by Swift, I decided to try my hand at using UserDefaults instead, which would permit me wrap the properties in @State. This led to many more issues. Long story short, I experienced multiple challenges trying to figure out how to implement UserDefaults.standard.set on a bound TextField. I went so far as to attempt to write a custom Binding<String>, but I can't manage to read back that data later for use in the Double instance. Here's the code below as it was when I ran into the initial issue with the view not updating:

TextField("Total Hours", text: $totalHours)
                    .keyboardType(.decimalPad)
                    .focused($hoursIsFocus)
                    .toolbar {
                        ToolbarItemGroup(placement: .keyboard) {
                            if hoursIsFocus {
                                Button("Done") {
                                    hoursIsFocus = false
                                    doubleTotalHours = Double(totalHours)!
                                }
                            }
                        }
Text("Total Hours")
                    .font(.subheadline)
                    .frame(alignment: .center)
                if Hours().doubleTotalHours >= 40.0 {
                    Image(systemName: "checkmark.circle")
                        .font(.system(size: 25))
                        .foregroundColor(.green)
                        .frame(width: 100, alignment: .center)
                } else {
                    Image(systemName: "x.circle.fill")
                        .font(.system(size: 25))
                        .foregroundColor(.red)
                        .frame(width: 100, alignment: .center)
                }

How can I save the $totalHours to UserDefaults and use that vale to update an instance of @State var doubleTotalHours = 0.0 so that my view will update? Any ideas would be sincerely appreciated.

Thanks, Andy

3      

Why do you not just save the number from text field to @AppStorage

@AppStorage("totalHours") var totalHours: Double = 0
TextField("Total Hours", value: $totalHours, format: .number)
  .textFieldStyle(.roundedBorder)
  .padding()

3      

[https://www.hackingwithswift.com/forums/swiftui/persisting-textfield/24424/24426]

Thanks Nigel! That did help quite a bit, but failed to resolve the core issue that I was experiencing with respect to updating a view based on the value. I've tried a few different iterations of the same thing, but ultimately, when the value changes in the TextField the view fails to see it and update.

I've tried a few different ways to make it work, including writting an ObservedObject class to and @Published instance but I'm still having the same issue. I've also tried updating an @State varibale when the user inputs a value, but that also doesn't seem to change the view at all.

Text("Total Hours")
                    .font(.subheadline)
                    .frame(alignment: .center)
                if Hours().doubleTotalHours >= 40.0 {
                    Image(systemName: "checkmark.circle")
                        .font(.system(size: 25))
                        .foregroundColor(.green)
                        .frame(width: 100, alignment: .center)
                } else {
                    Image(systemName: "x.circle.fill")
                        .font(.system(size: 25))
                        .foregroundColor(.red)
                        .frame(width: 100, alignment: .center)
                }

This chunk reads the value from a variable in a different file, which is updated when the user submits a new value to the TextField. I can validate that the value changes when the "Done" button is pressed, but the view fails to see it. It's the same core issue I've been fumbling with and how I got to triyng to convert the value before (just trying to figure out the view).

To elaborate on this: The view will change state as appropriate when the app is closed and relaunched, but it will not change state when the variable is updated.

Any ideas what I'm doing wrong? Thanks so much for your help. Andy

3      

Hi @CodingPilot Not quite sure what you trying to do with Hours() But you saving to @AppStorage

struct ContentView: View {
    @AppStorage("DoubleTotalHours") var doubleTotalHours: Double = 0.0

    var body: some View {
        VStack(spacing: 50) {
            TextField("Number of Hours", value: $doubleTotalHours, format: .number)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .padding()
    }
}

And the other view

struct AnotherView: View {
    @AppStorage("DoubleTotalHours") var doubleTotalHours: Double = 0.0

    var body: some View {
        Text("\(doubleTotalHours)")
            .font(.subheadline)
            .frame(alignment: .center)

        if doubleTotalHours >= 40.0 {
            Image(systemName: "checkmark.circle")
                .font(.system(size: 25))
                .foregroundColor(.green)
                .frame(width: 100, alignment: .center)
        } else {
            Image(systemName: "x.circle.fill")
                .font(.system(size: 25))
                .foregroundColor(.red)
                .frame(width: 100, alignment: .center)
        }
    }
}

3      

Hey @NigelGee

So Hours() refers to the Hours() struct located in a different SwiftUI file.

struct Hours: View {
    let minimumTotalHours = 40.0
    let minimumNightHours = 3.0
    let minimumSoloHours = 15.0
    let MinimumCrossCountryHours = 5.0

    @AppStorage("TotalHours") var totalHours: Double = 0.0

    @AppStorage("NightHours") var nightHours: String = ""
    @AppStorage("DoubleNightHours") var doubleNightHours: Double = 0.0

    @AppStorage("SoloHours") var soloHours: String = ""
    @AppStorage("DoubleSoloHours") var doubleSoloHours: Double = 0.0

    @AppStorage("CrossCountryHours") var crossCountryHours: String = ""
    @AppStorage("DoubleCrossCountryHours") var doubleCrossCountryHours: Double = 0.0

    @FocusState private var hoursIsFocus: Bool

    var body: some View {

I'm attempting to call Hours().totalHours from a different file in the same project. That file is where all information to the pilots hours-based experience lives. I did it this way because I have a variety of other calculations to do (time represented as a double, time represented on a calendar, validation represented as bools) and am segmenting them by file for ease of reference.

The app will load the appropriate view at the launch of the app, but does not load the appropriate view when the Hours().totalHours value is updated.

Here is ContentView:

struct ContentView: View {

    var body: some View {
            NavigationView {
                VStack {
                    HeaderView()
                    Form{
                        Section {
                            NavigationLink("Hours Requirements") { Hours() }
                            NavigationLink("Endorsements") { Endorsements() }
                            NavigationLink("Medical Qualification") { Medical() }
                            NavigationLink("Airmen Certification Standards") { ACS() }

                        }
                        Section {
                            Text("Minimum Requirements")
                            Text("How To Use This App")
                            NavigationLink("Settings") { Settings() }
                        }
                    }
                }
            }
            Text("Version 0.1B")
                .font(.caption2)
                .frame(maxWidth: .infinity, maxHeight: 1)

        }
    }

The issue appears in the HeaderView file. The presents fine on in ContentView, but does not update. Here is the HeaderView file, only down to the first view that is not changing state (there are six, but the issue is mostly the same for all of them and they are all similar if statements:

struct HeaderView: View {
    @State private var datesExplanationShowing = false
    @State private var nightExplanationShowing = false
    @State private var nightReqMet = false
    @StateObject var totalHours = ObservedHours()

    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(height: 100)

            VStack {
                Text("Student Pilot Dashboard")
                    .font(.headline)
                    .foregroundColor(.white)
                Text("Private Pilot Edition")
                    .font(.footnote)
                    .foregroundColor(.white)
                Text("Hello, \(Settings().pilotName)!")
                    .foregroundColor(.white)
            }

        }
        HStack {
            VStack {
                Text("Total Hours")
                    .font(.subheadline)
                    .frame(alignment: .center)
                if totalHours.totalHours >= 40.0 {
                    Image(systemName: "checkmark.circle")
                        .font(.system(size: 25))
                        .foregroundColor(.green)
                        .frame(width: 100, alignment: .center)
                } else {
                    Image(systemName: "x.circle.fill")
                        .font(.system(size: 25))
                        .foregroundColor(.red)
                        .frame(width: 100, alignment: .center)
                }
            }

Any help would be greatly appreciated with this. Is it because I have nested a view inside of a view inside of a view? Have I created a sort of... SwiftUI Inception?

Thanks, Andy

3      

Think you got the it going the wrong way around.

struct ContentView: View {
struct ContentView: View {
    @AppStorage("TotalHours") var totalHours: Double = 0.0

    var body: some View {
        VStack {

            HeaderView(totalHours: totalHours)

            Form {
                NavigationLink("Hours Requirement") {
                    Hours(totalHours: $totalHours)
                }
            }
            .padding()
        }
    }
}

Then the HeaderView

struct HeaderView: View {
    let totalHours: Double

    var body: some View {
        HStack {
            Text(totalHours, format: .number)

            if totalHours >= 40 {
                Image(systemName: "checkmark.circle")
                    .font(.system(size: 25))
                    .foregroundColor(.green)
                    .frame(width: 100, alignment: .center)
            } else {
                Image(systemName: "x.circle.fill")
                    .font(.system(size: 25))
                    .foregroundColor(.red)
                    .frame(width: 100, alignment: .center)
            }
        }
        .padding()
    }
}

and what to navigation to Hours

struct Hours: View {
    @Binding var totalHours: Double

    var body: some View {
        TextField("Change Hours", value: $totalHours, format: .number)
            .textFieldStyle(.roundedBorder)
            .padding()
    }
}

3      

@NigelGee Thanks for the help! That's not quite the solution that I found to work for me but it did lead me to the solution I needed. Effectively, I made a new @AppStorage("TotalHours") var in the HeaderView file. This gets updated at the same time as the variable in the Hours file. Long story short, this works!

Thank for all your help! Andy

3      

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!

Reply to this topic…

You need to create an account or log in to reply.

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.