I probably worked too much today..
In this tiny app when the button is clicked a new view must appear after 3 seconds hVideoURL variable is assigned a value (which is not NIL). So a new view (sheet) should appear with the text "IS NOT NIL"..
But for some reason when it's shown i see "wow, it's nil", which means variable still has no value.
Why i that? What am i missing?
struct ContentView: View {
#State var hVideoURL: URL? = nil
#State var isPaused: Bool = false
var body: some View {
Button("Let's Go!") {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("settings isPaused to TRUE")
self.hVideoURL = URL(string: "https://firebasestorage.googleapis.com/v0/b/fitma-e3043.appspot.com/o/flamelink%2Fmedia%2F1-horizontal.mov?alt=media&token=8f7dfc0f-0261-4a78-9eb0-6154ce1d9dfe")
print("[debug] hVideoURL = \(hVideoURL)")
self.isPaused = true
}
}
.sheet(isPresented: self.$isPaused, onDismiss: {
self.isPaused = false
print("resume playing main video")
}) {
detailedVideoView
}
}
#ViewBuilder
var detailedVideoView: some View {
if self.hVideoURL != nil {
VStack {
Text("IS NOT NIL")
}
} else {
Text("wow, it's nil")
}
}
}
Related
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() */
}
}*/
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
I need to show a login screen when the user session is expired. I tried to achieve this by changing the current window:
#main
struct ResetViewHierarchyApp: App {
#StateObject private var state = appState
var body: some Scene {
WindowGroup {
if state.isLoggedIn {
ContentView()
} else {
LogInView()
}
}
}
}
When no modal views are presented then it works fine. If only one modal view is presented, it also works, the modal view is dismissed. But if there are more than one modal views are presented, then the root view is replaced, but only the topmost modal view is dismissed. Here is ContentView:
struct ContentView: View {
#State private var isPresentingSheet1 = false
#State private var isPresentingSheet2 = false
var body: some View {
Text("Hello, world!")
.padding()
Button(action: {
isPresentingSheet1 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet1) {
sheetView1
}
}
}
private extension ContentView {
var sheetView1: some View {
VStack {
Text("Sheet 1")
.padding()
Button(action: {
isPresentingSheet2 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet2) {
sheetView2
}
}
}
var sheetView2: some View {
VStack {
Text("Sheet 2")
.padding()
Button(action: {
appState.isLoggedIn = false
}, label: {
Text("Log Out")
.padding()
})
}
}
}
The same happens if I use fullScreenCover instead of sheet.
Does anybody know how to solve this issue, to dismiss all the presented modals at once?
I've solved this issue with UIKit windows:
#StateObject private var state = appState
#State private var contentWindow: UIWindow?
var body: some Scene {
WindowGroup {
EmptyView()
.onAppear {
updateContentWindow(isLoggedIn: state.isLoggedIn)
}.onReceive(state.$isLoggedIn) { isLoggedIn in
updateContentWindow(isLoggedIn: isLoggedIn)
}
}
}
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
func updateContentWindow(isLoggedIn: Bool) {
contentWindow?.isHidden = true
contentWindow = nil
if let windowScene = window?.windowScene {
contentWindow = UIWindow(windowScene: windowScene)
contentWindow?.windowLevel = UIWindow.Level.normal
if isLoggedIn {
contentWindow?.rootViewController = UIHostingController(rootView: ContentView())
} else {
contentWindow?.rootViewController = UIHostingController(rootView: LogInView())
}
contentWindow?.makeKeyAndVisible()
}
}
It is indeed a strange bug.. however I found a workaround for it.
You can keep your States of the modal View inside your Observable / Environment Object. When logging out, you have to make sure to close all your sheets.
Here is a example:
First adding showSheet as Published Value in the AppState
class AppState : ObservableObject {
#Published var isLoggedIn : Bool = true
#Published var showSheet1 : Bool = false
#Published var showSheet2 : Bool = false
}
When logging out, turn all your sheets to false.
Button(action: {
self.state.isLoggedIn = false
self.state.showSheet1 = false
self.state.showSheet2 = false
}, label: {
Text("Log Out")
.padding()
})
Of course you have to use these values in your Button for toggling sheet and in your sheet.
.sheet(isPresented: $state.showSheet2) {
Edit:
Even simpler, you don't have to manually set it to false in the LogOut action. Instead do it all in the appState
#Published var isLoggedIn : Bool = true {
willSet {
if newValue == false {
showSheet1 = false
showSheet2 = false
}
}
}
I have a sign out button on a modal sheet that takes the user back to the login screen. To accomplish this I first dismiss the sheet and then, using asyncAfter(deadline:) I set an environment variable that causes the login page to appear. Everything works fine, but once the sheet is dismissed, the transition from the view under the sheet to the login page is pretty jarring. Mostly because there isn't one. The top view just disappears, revealing the login view. I know I can create custom transitions, but I can't figure out where to attach it. Say, for example, I want to fade out the view underneath the sheet. (Although, I'm open to any kind of transition!)
This is the struct that directs the traffic:
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = UserDefaults.standard.bool(forKey: "FirstVisit")
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
LoginView()
} else if tower.currentPage == .idle {
LoginView()
}
}.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
And this is the sign out button on the sheet:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
Here is a simplified demo on your replicated code (and I made some longer delay to make it mode visible). Of course you will need to tune it for your needs by changing type of transition or animation, etc. Tested with Xcode 12 / iOS 14
class Tower: ObservableObject {
enum PageType {
case onboarding, login, idle
}
#Published var currentPage: PageType = .onboarding
}
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = false
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
Text("LoginView")
.transition(.move(edge: .trailing)) // << here !!
} else if tower.currentPage == .idle {
Text("IdleView")
}
}
.animation(.default, value: tower.currentPage) // << here !!
.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
struct Onboarding1View: View {
#EnvironmentObject var tower: Tower
#Environment(\.presentationMode) var presentationMode
#State private var isPresented = true
var body: some View {
Text("Login")
.sheet(isPresented: $isPresented) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
}
}
}
In SwiftUI I've created a struct that should create different overlay views depending on some state variables. If any of the state booleans is true, then it should return custom view (either ErrorOverlay or LoadingOverlay or else an EmptyView) like this:
struct OverlayContainer: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
Now I've implemented the overlay on some content in the Home view with buttons that should change the state and show the correct overlay, like this:
struct Home: View {
var body: some View {
let overlayContainer = OverlayContainer()
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}
This isn't working: when I click the button nothing happens. Why and how to solve this? (without using binding, see below)
ps. I've been able to get a working solution by doing the following:
extracting the state booleans to the Home view
pass these through the constructor of the OverlayContainer
change the state booleans instead of calling the set methods when clicking the buttons
change the OverlayContainer so it implements an init method with both booleans
change the state booleans in the OverlayContainer to bindings.
However, I'd like to implement the states in the OverlayContainer to be able to re-use that in different screens, without implementing state variables in all of these screens. Firstly because there will probably be more cases than just these 2. Secondly because not all screens will need to access all states and I haven't found out a simple way to implement optional bindings through the init method.
To me it feels that all these states belong to the OverlayContainer, and changing the state should be as short and clean as possible. Defining states everywhere feels like code duplication. Maybe I need a completely different architecture?
It should be used Binding instead. Here is possible solution.
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
var body: some View {
HStack {
// Some more content here
Button(action: {
self.isLoading = true
}) {
Text("Start loading")
}
Button(action: {
self.isErrorShown = true
}) {
Text("Show error")
}
}.overlay(OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown))
}
}
To make it the way you want, use Binding:
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
self.isErrorShown = !isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
self.isLoading = !isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading = false
#State var isErrorShown = false
var body: some View {
let overlayContainer = OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown)
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}
I want to show my new view after the success login process with Firebase, my signUp and Recover password are already working because I'm showing it as a sheet but in this one, I want to show a new view, I have tried with NavigationLink, with onReceive but I have been unable to do this work.
struct LoginView: View {
#ObservedObject var viewModel = ViewModel()
#State private var formOffset: CGFloat = 0
#State private var presentSignUpSheet = false
#State private var presentPasswordRecoverySheet = false
#State private var presentLobbySheet = false
var body: some View {
VStack {
HeaderView(title: Constants.appName)
Spacer()
Divider()
Group {
BodyView(value: viewModel).viewSelection(view: Constants.QuestionnaireView.signIn.rawValue)
LCButton(text: Constants.login) {
self.viewModel.signIn()
}.alert(isPresented: $viewModel.thereIsAnError) {
Alert(title: Text(Constants.alert), message: Text(viewModel.errorMessage), dismissButton: .default(Text(Constants.ok)))
}
Button(action: {
self.presentSignUpSheet.toggle()
}) {
Text(Constants.signUp)
}.sheet(isPresented: $presentSignUpSheet) {
SignUpView()
}.padding()
Button(action: {
self.presentPasswordRecoverySheet.toggle()
}) {
Text(Constants.forgotPassword)
}.sheet(isPresented: $presentPasswordRecoverySheet) {
RecoverPasswordView()
}.padding()
}
}.edgesIgnoringSafeArea(.top)
.padding()
.offset(y: self.formOffset)
}
}
class ViewModel: ObservableObject {
#Published var user = User()
#Published var confirmPassword = ""
#Published var thereIsAnError = false
#Published var errorMessage = ""
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
var onSuccessLogin = PassthroughSubject<Bool, Never>()
private var shouldPopView = false {
didSet {
viewDismissalModePublisher.send(shouldPopView)
}
}
private var shouldShowLobbyView = false {
didSet {
onSuccessLogin.send(shouldShowLobbyView)
}
}
func registerSuccess() {
self.user.email = ""
self.user.password = ""
self.confirmPassword = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
func signUpProcess() {
if user.password != confirmPassword {
errorMessage = Constants.passConfirmWrong
thereIsAnError.toggle()
} else {
signUp()
}
}
func signUp() {
Auth.auth().createUser(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.registerSuccess()
}
}
}
func signIn() {
Auth.auth().signIn(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.shouldShowLobbyView.toggle()
self.user.email = ""
self.user.password = ""
}
}
}
func recoverPassword() {
Auth.auth().sendPasswordReset(withEmail: user.email) { (error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.user.email = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
}
}
}
I had a similar problem and I found a solution that mimics some navigation
I was going to write the basics here but nothing better then posting the source with explanation.
ViewRouter Tutorial
This consists in a ObservableObject as EnvironmentObject and it allows you to show views in full screen, rather than sheets.
In your case you would have a LoginView and in this view you could open sheets for the Signup and Recover Password views and the LobbyView opening as fullscreen view, like it would in UIKit with the present method