For iOS16+, I am using Google Sign In button on ContentView to authenticate users. If users are not authenticated, the button shows and user can go through the authentication process. Once authenticated, user is brought back to ContentView that also checks if user has been signed in. If so, we send the user to main app, say UserProfileView.
My code is as follows. When the user is signed in, UserProfileView renders inside ContentView rather than sending the user new a completely new view. I've tried various ways but can't figure out how to do so.
import SwiftUI
import GoogleSignIn
import GoogleSignInSwift
struct ContentView: View {
//some vars declared
#State private var path = NavigationPath()
#EnvironmentObject var authViewModel: AuthenticationViewModel
#ObservedObject var vm = GoogleSignInButtonViewModel(style: .wide)
var body: some View {
VStack {
//some code with logo etc
HStack {
switch authViewModel.state {
case .signedIn:
let _ = print("User is signed in")
NavigationStack(path: $path) {
Text("")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
path.append("UserProfileView")
}
}
case .signedOut:
let _ = print("User is signed out")
GoogleSignInButton(viewModel: vm, action: authViewModel.signIn)
.accessibilityIdentifier("GoogleSignInButton")
.accessibility(hint: Text("Sign in with Google button."))
.padding()
}
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color("Standard").ignoresSafeArea(.all, edges: .all))
}
ContentView before user signs in:
ContentView after user signs in; note the UserProfileView renders inside the ContentView (where the Google Sign In button used to be), rather than navigating to a UserProfileView that takes up the entire screen.
UserProfileView - user should be directed from ContentView to here upon login. UserProfileView as shown below takes up the whole screen, which is what I want.
Would appreciate some help here.
How about this, just using a SignInState enum to switch between views:
enum SignInState {
case signedIn, signedOut
}
struct ContentView: View {
#State var state: SignInState
var body: some View {
switch state {
case .signedOut:
SignInView(state: $state)
case .signedIn:
NavigationStack {
UserProfileView()
}
}
}
}
struct SignInView: View {
#Binding var state: SignInState
var body: some View {
VStack {
Rectangle()
.frame(width: 300, height: 400)
Button("Sign in") {
state = .signedIn
}
.buttonStyle(.borderedProminent)
.padding()
Spacer()
}
.frame(maxWidth: .infinity)
.background(.mint, ignoresSafeAreaEdges: .all)
.navigationTitle("Sign in")
}
}
struct UserProfileView: View {
var body: some View {
Text("User Profile")
.navigationTitle("User Profile")
}
}
Related
I'm pretty new to SwiftUI, so I'm wondering how to navigate to a new view only when an async method returns with a value. Here is my view:
import SwiftUI
struct FollowersView: View {
#State var followers: [Follower]
#State var user: User?
#State private var selection: String? = nil
#StateObject private var viewModel = GitHubUsersViewModel()
let columns = [
GridItem(.flexible(minimum: 150, maximum: 175)),
GridItem(.flexible(minimum: 150, maximum: 175)),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(followers, id: \.self) { follower in
FollowerCardView(follower: follower)
.onTapGesture {
Task {
self.user = try await self.viewModel.manager.getUser(for: follower.login)
self.selection = NavigationTags.userFound
}
}
}
}
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
}
}
When self.user gets populated, I want to be able to navigate to another view. But, I can't figure out where to put the NavigationLink.
This FollowersView is already embedded in a NavigationView from within it's parent view.
Please advise?
For iOS 15 you can still use NavigationLink as it isn't deprecated until iOS 16 when the new NavigationStack stuff comes in.
Anyway...
struct SomeView: View {
#State var shouldNavigate: Bool = false
var body: some View {
NavigationLink(
isActive: $shouldNavigate,
destination: navDestination
) {
Text("Press me")
.onTapGesture {
let result = await someAsyncFunction()
shouldNavigate = true
}
}
}
#ViewBuilder
var navDestination: some View {
Text("Ta dah!")
}
}
This will navigate to your destination when the async function completes and sets shouldNavigate to true.
There are nmany ways that you could do this. This is just one of them. Good luck
As a side note, you need to make sure your view is part of a NavigationView too.
Hi I am pretty new to using SwiftUI. I have been having trouble with adding a tap gesture to my welcome page that will allow the user to move on to another view with drop down question boxes. I currently have this:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "Globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Welcome to WOD planner!")
.font(.title)
.multilineTextAlignment(.center)
.onTapGesture{ action:{
//go to question page
}
}
}
.padding()
}
}
I made another view labeled QuestionPage. Im just confused how to code this gesture to make it change and isolate the two different views.
Thanks!
I solve this by having a State variable that will track the View that I want to show. Within the WindowGroup in my App, I now present the View that the viewState refers to.
This sample code should do the trick, it will change Views if you tap on the text in the middle of the screen. I personally would rather use a button, but it can be done with onTapGesture:
enum ViewState {
case welcomeView
case questionView
}
#main
struct MyApp: App {
#State var currentView: ViewState = .welcomeView
var body: some Scene {
WindowGroup {
switch (currentView) {
case .welcomeView:
WelcomeView(viewState: $currentView)
case .questionView:
QuestionView(viewState: $currentView)
}
}
}
}
struct WelcomeView: View {
#Binding var viewState: ViewState
var body: some View {
// your view here
Text("Welcome")
.onTapGesture { viewState = .questionView }
}
}
struct QuestionView: View {
#Binding var viewState: ViewState
var body: some View {
// your view here
Text("Some questions")
.onTapGesture { viewState = .welcomeView }
}
}
import SwiftUI
struct LoginView: View {
#ObservedObject var vm : ViewModel
#State private var username = ""
private var searchAllowed : Bool{
if(username.count>2)
{
return false
}
return true
}
var body: some View {
NavigationView{
VStack{
Text("Enter your Username:")
.font(.title.bold())
TextField("Username", text: $username)
.frame(width: 280)
.padding(.bottom)
NavigationLink{
ProfileView(vm: vm)
} label:
{
ZStack{
RoundedRectangle(cornerRadius: 5)
.fill(.gray)
.frame(width: 200, height: 50)
Text("Search")
.foregroundColor(.white)
.task {
await vm.fetchPlayerData()
}
}
}
.disabled(searchAllowed)
}
}
}
}
So I want it so that when I press the search NavigationLink it'll only take me to the view if the api call was sucessful and I got some particular piece of data I needed.
How can I make it conditional like this? It's all sync so it seems like I can't really wait to see if I have the right data before moving the user into the new View.
As #NoeOnJupiter said:
In your NavigationLink destination, use conditional view logic, that shows ProgressView. After your loadData() finishes, flip a boolean and show your desired View showing API Data.
I am stuck on linking my login screen to my Main screen. Both have been created separately and I have used the Button function to create the login button and it goes to another screen with the email I have logged in with but that's not what I want as I want my home screen to open up instead.
Button(action: model.login) {
Text("LOGIN")
.fontWeight(.bold)
.foregroundColor(Color("Color1"))
.padding(.vertical)
.frame(width: UIScreen.main.bounds.width - 30)
.background(Color.white)
.clipShape(Capsule())
}
.padding(.top, 22)
My MainView code starting
struct MainView: View {
#State private var isShowing = false
var body: some View {
NavigationView {
ZStack {
In your App struct add a State var isLoggedin passing it to the loginView and toggling it from there.
This is how I implemented.
struct TestApp: App {
#State var isLoggedin: Bool = false
var body: some Scene {
WindowGroup {
if isLoggedin {
ContentView()
} else {
LoginView(isLoggedin: $isLoggedin)
}
}
}
}
struct LoginView: View {
#Binding var isLoggedin: Bool
var body: some View {
Button(action: {
isLoggedin = true
}, label: {
Text("Login")
})
}
}
This way you can pick which view to show. ContentView to LoginView.
I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse