Ed Jones
Ed Jones

Reputation: 341

SwiftUi - Changing Views Via a function

I'm struggling in a big way to get some basic code working that allows me to change a view in a Swiftui project.

I have 3 views: my default ContentView, a login screen and a main menu.

The project loads to ContentView which is just a logo. I have a boolean value which defaults to false and an extension function which either loads the login screen, or main menu dependent on the value of that boolean.

This part is working fine, project loads and i see the login page. The login button calls a function which does a URLsession, and depending on the returned value from that, sets the boolean flag to true or leaves it as false in the case of a failed login.

The bit im struggling with is getting the function to change the view. I can toggle the boolean flag in the function fine, but if I include a statement such as MainMenu() to load my main menu view, nothing happens.

I have experimented with observable objects and "subscribers" to try to get this working but i'm not sure if this is actually needed and I had no joy getting it working.

any help is greatly appreciated

Full code:

import SwiftUI
 
var isLoggedin = false
var authenticationFailure = false
 
    func DoLogin(username: inout String, password: inout String){
 
 
        print(isLoggedin)
 
        let url = URL(string: "https://www.example.com/mobile/ios/test.php")!
        var request = URLRequest(url: url)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        let parameters: [String: Any] = [
            "username": username,
            "password": password]
 
 
        request.httpBody = parameters.percentEncoded()
 
        let task = URLSession.shared.dataTask(with: request) { data,response,error in
            guard let data = data,
                  let response = response as? HTTPURLResponse,
                  error == nil else{
                print("error", error ?? "Unknown error")
                return
            }
 
            guard (200 ... 299) ~= response.statusCode else {
                print("statuscode should be 2xx, got \(response.statusCode)")
                print("response = \(response)")
                return
            }
 
            let responseString = String(data: data, encoding: .utf8)
            if responseString == "1"{
                print("Logged in")
                isLoggedin = true
                print(isLoggedin)
                MainMenu()
            }
            else{
                print("NO LOGIN")
                isLoggedin = false
            }
        }
        task.resume()
    }
 
extension View {
  @ViewBuilder func changeView(_ isLoggedin: Bool) -> some View {
    switch isLoggedin {
    case false: LoginView()
    case true: MainMenu()
    }
  }
}
 
struct ContentView: View {
 
    @State var isLoggedin = false
 
    var body: some View {
        Color.clear
        .changeView(isLoggedin)
        VStack{
            Image("logo")
                .padding(.bottom, 40)
 
      }
 }
 
}
 
struct LoginView: View {
 
    @State var username: String = ""
    @State var password: String = ""
 
    @State var isLoggedin = false
 
    var body: some View {
 
 
 
        VStack{
 
            Form{
            TextField("Username: ", text:$username)
                .frame(maxWidth: .infinity, alignment: .center)
                .autocapitalization(.none)
            SecureField("Password: ",text:$password)
            Button("Login"){
                DoLogin(username: &username, password: &password)
                }
 
 
            }
 
        .padding(.top, 100)
 
        }
 
 
    }
 
 
}
 
 
 
 
struct MainMenu: View{
 
    @State var isLoggedin = true
 
    var body: some View{
            VStack{
                    Text("Main Menu")
 
            }
        }
 
    }
 
/*struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        /*ContentView() */
    }
}*/

Upvotes: 1

Views: 1256

Answers (1)

Abizern
Abizern

Reputation: 150565

You have some problems with your code.

In your Content view

@State var isLoggedin = false

isn't being changed by anything inside the body of the struct, so it is always going to be false.

Your LoginView calls doLogin but it doesn't change any variables that the views use to render themselves. In the body of your doLogin method it is returning views, but it isn't returning them to anything.

Here is an example that does sort of what you want. shows different screens depending on state. SwiftUI shows views depending on states, so you need to change states to show different views. I've done this in one file so it's easier to show here:

import SwiftUI

class ContentViewModel: ObservableObject {
    enum ViewState {
        case initial
        case loading
        case login
        case menu
    }
    @Published var username = ""
    @Published var password = ""
    @Published var viewState = ViewState.initial

    var loginButtonDisabled: Bool {
        username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty  ||
            password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    func goToLogin() {
        viewState = .login
    }

    func login() {
        viewState = .loading
        // I'm not actually logging in, just randomly simulating either a successful or unsuccessful login after a short delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            if Bool.random() {
                self.viewState = .menu
            } else {
                self.viewState = .login
            }
        }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()

    var body: some View {
        ZStack {
            initialView
            loginView
            loadingView
            menuView
        }
    }

    private var initialView: InitialView? {
        guard .initial ==  viewModel.viewState else { return nil }
        return InitialView(viewModel: viewModel)
    }

    private var loginView: LoginView? {
        guard .login == viewModel.viewState else { return nil }
        return LoginView(viewModel: viewModel)
    }

    private var loadingView: LoadingView? {
        guard .loading == viewModel.viewState else { return nil }
        return LoadingView()
    }

    private var menuView: MenuView? {
        guard .menu == viewModel.viewState else { return nil }
        return MenuView()
    }

}

struct InitialView: View {
    @ObservedObject var viewModel: ContentViewModel
    var body: some View {
        VStack {
            Text("Initial View")
                .font(.largeTitle)
                .padding()
            Button("Login") { viewModel.goToLogin() }

        }
    }
}

struct LoginView: View {
    @ObservedObject var viewModel: ContentViewModel
    var body: some View {
        VStack {
            Text("Login View")
                .font(.largeTitle)
                .padding()
            TextField("Username", text: $viewModel.username)
                .padding()
            TextField("Password", text: $viewModel.password)
                .padding()
            Button("Login") {viewModel.login() }
                .padding()
                .disabled(viewModel.loginButtonDisabled)
        }
    }
}

struct LoadingView: View {
    var body: some View {
        Text("Loading View")
            .font(.largeTitle)
    }
}

struct MenuView: View {
    var body: some View {
        Text("Menu View")
            .font(.largeTitle)
    }
}

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

This is all driven off one view model which publishes an enum state that is used by ContentView to show the different views. This is possible because in groups (such as the ZStack), a nil view is not rendered.

You can clone a project with this from https://github.com/Abizern/SO-68407322

Upvotes: 1

Related Questions