Home > Software engineering >  (SwiftUI) How to make text hidden when view loads and become visible once toggle is tapped
(SwiftUI) How to make text hidden when view loads and become visible once toggle is tapped

Time:03-16

I'm trying to create a view with a toggle. the toggle changes ui preferences. when the toggle is changed it changes the Text below. as it stands the easiest way to do this is

        VStack {
            Toggle("toggle me", isOn: $isOn)
            Text( isOn ? "I've been toggled once more" : "I've been toggled again" )
         }

what I would like is for the Text( isOn ? "I've been toggled once more" : "I've been toggled again" ) to be hidden until after the toggle is interacted with. it doesn't matter if it's been toggled on or off, I would like to make the Text visible once the toggle has been interacted with. I've tried to use the .onChange(of: perform: ) but perform: requires a void method. so I can't put the Text in there. I have tried using a boolean/opacity fix as shown in the answer here but same problem. can't change a Boolean value in perform. only void methods are allowed. I did try to use onTapGesture but that didn't work at all. I had a print statement in there that never once printed. not sure what else to do. is it possible to keep text hidden until the toggle has been interacted with? here's my code

struct ProfileView: View {
    
    @State private var isOn = UserDefaults.standard.bool(forKey: "isOn")
    @State private var showText = false
    
    var body: some View {
        VStack { // vstack 0
            Text("Profile Page")
                .font(.title)
            
            Text("Are you ready to toggle?")
                .font(.title2)
                .multilineTextAlignment(.center)
            
            VStack { // vstack 1
                Toggle("toggle this", isOn: $isOn)
                    .onTapGesture {
                        print("toggled") // this never prints
                    }
//                    .onChange(of: isOn, perform: { value in
//                        value ? Text("yes") : Text("no")
//                        print("value = \(value)")
//                    })
// the Text here never shows up. and can't set showText = true here
                Text( isOn ? "Thank you." : "Toggled again" )
                    .opacity(showText ? 1 : 0)
// from the linked stackoverflow answer. trying to make this boolean work. is it a lost cause? 
            } // vstack 1
        } // vstack 0
        .frame(
            width: UIScreen.main.bounds.width,
            height: UIScreen.main.bounds.height - 200,
            alignment: .top)
    }
}

So is what I'm asking even possible? Thanks for the help!

CodePudding user response:

You can continue to use a @State variable for showText, which, according to your requirements, should only be shown once the Toggle is interacted with. Rather than using a @State variable that is initialized with a UserDefaults value, you probably want to use @AppStorage for isOn.

struct ProfileView: View {
    
    @AppStorage("isOn") private var isOn : Bool = false //@AppStorage syncs this value with UserDefaults automatically
    @State private var showText = false
    
    var body: some View {
        VStack { // vstack 0
            Text("Profile Page")
                .font(.title)
            
            Text("Are you ready to toggle?")
                .font(.title2)
                .multilineTextAlignment(.center)
            
            VStack { // vstack 1
                Toggle("toggle this", isOn: $isOn)
                    .onChange(of: isOn, perform: { value in
                        showText = true //Once the toggle is interacted with, set showText to true
                    })
                if showText { //only show the text if showText == true
                    Text( isOn ? "Thank you." : "Toggled again" )
                }
            } // vstack 1
        } // vstack 0
        .padding(.bottom, 200) //try not to use UIScreen dimensions in SwiftUI -- default to using padding instead when possible
    }
}

CodePudding user response:

Here is a slightly different take on how to solve this. You can have @AppStorage store an optional value, so long as you extend optionals to conform to RawRepresentable. This allows you to eliminate the showText state variable, and allows you a definite state with 3 possibilities, true, false and nil. Since you don't want the text to show up until the Toggle has been toggled once, the nil state allows you to know this. Once Toggle has been toggled, isOn will have either a true or false value from that point forward, unless you set it somewhere else to nil again.

There is one complexity that this adds. Toggle does not take an Bool?. Therefore, you have to write your own binding for it so that there is a definite value being passed in to the Toggle.

struct ProfileView: View {
    
    @AppStorage("isOn") private(set) var isOn: Bool? = nil

    var body: some View {
        VStack { // vstack 0
            Text("Profile Page")
                .font(.title)
            
            Text("Are you ready to toggle?")
                .font(.title2)
                .multilineTextAlignment(.center)
            
            VStack { // vstack 1
                Toggle("toggle this", isOn: Binding<Bool>(
                    get: { isOn ?? false },
                    set: { isOn = $0 }
                ))
                    .onTapGesture {
                        print("toggled") // this never prints
                    }
                Text( isOn == true ? "Thank you." : "Toggled again" )
                    // Compare isOn to nil, instead of use showText
                    .opacity(isOn != nil ? 1 : 0)
            } // vstack 1
            Spacer()
        } // vstack 0
        .padding()
    }
}

// This extension allows Optionals to be written to @AppStorage
extension Optional: RawRepresentable where Wrapped: Codable {
    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let json = String(data: data, encoding: .utf8)
        else {
            return "{}"
        }
        return json
    }

    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let value = try? JSONDecoder().decode(Self.self, from: data)
        else {
            return nil
        }
        self = value
    }
}
  • Related