I just implemented Sign In With Apple in my SwiftUI App using the examples I found on the web.
Works great but I have a very important question: Where should I do the Check if the user is validated or not?
I have a function checkUserAuth() that returns 3 states: undefined, signedOut and signedIn. Based on each one of those returns, I want to open a different View.
self.signInWithAppleManager.checkUserAuth { (authenticationState) in
switch authenticationState {
case .undefined:
print("AuthenticationState: .undefined")
case .signedOut:
print("AuthenticationState: .signedOut")
case .signedIn:
print("AuthenticationState: .signedIn")
}
}
If my function returns .undefined or .signedOut, I want to open the LoginView.swift. If the function returns .signIn, I want to open the MainView.swift.
Where should I make this Check?
Should I do that on the SceneDelegate.swift and show the right View or should a have a View responsible to make this validation and "navigate" to the right view?
Thanks I hope I could explain my problem.
My fast solution suggests to persist this state in some #Published var and check it in the needed view. It does not looks good, maybe better create checking functions somewhere else (like viewModel), but I think it will direct you to the right way:
import SwiftUI
import Combine
final class SingSingInWithAppleManager: ObservableObject {
#Published var userAuthState: UserAuthInAppleState
init() { // here you can check auth status
userAuthState = .undefined
}
func logIn() {
// here you're making login in and change status
userAuthState = .signedIn
}
}
enum UserAuthInAppleState {
case undefined
case signedOut
case signedIn
}
struct SingInWithAppleExample: View {
#EnvironmentObject var signInWithAppleManager: SingSingInWithAppleManager
var body: some View {
ZStack {
if signInWithAppleManager.userAuthState == .undefined {
Button(action:
{
self.signInWithAppleManager.logIn()
}) {
Text("Login in!")
}
} else {
Text("singed in!")
}
}
}
}
// MARK: how to make it with animation
struct SingInWithAppleExample: View {
#EnvironmentObject var signInWithAppleManager: SingSingInWithAppleManager
var body: some View {
ZStack {
Button(action:
{
withAnimation {
self.signInWithAppleManager.logIn()
}
}) {
Text("Login in!")
}
.opacity(signInWithAppleManager.userAuthState == .undefined ? 1 : 0)
Text("singed in!")
.opacity(signInWithAppleManager.userAuthState == .signedIn ? 1 : 0)
}
}
}
I think you should set this manager in the SceneDelegate func if it's the first thing you want to show. Here is PreviewProvider example:
struct SingInWithAppleExample_Previews: PreviewProvider {
static var previews: some View {
SingInWithAppleExample()
.environmentObject(SingSingInWithAppleManager()) // only add this line of code
}
}
P.S can you share "examples you found on the web" please, soon I'll need to implement it too
Related
I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}
The root view of my onboarding process has a NavigationView. The root view of my app is a login page that also contains a NavigationView. That means when someone launches the app for the first time, they will go through the onboarding process and land at the login screen - resulting in a navigation view within a navigation view.
Is there a way to reset the view stack or simply remove the extra navigation view when necessary?
This is how I implemented #New Dev's solution below. First comes the Tower class. (The name helps me visualize the fact that it's an ObservableObject.) Its job is to keep track of the currentPage and let interested views know when it has changed.
import Foundation
import SwiftUI
import Combine
class Tower: ObservableObject {
enum Views {
case onboarding, login, dashboard
}
let objectWillChange = PassthroughSubject<Tower, Never>()
#Published var currentPage: Views = .onboarding {
didSet {
objectWillChange.send(self)
}
}
}
Next comes the ConductorView. It is notified by the Tower when currentPage changes, and loads the corresponding view.
struct ConductorView: View {
#EnvironmentObject var tower: Tower
var body: some View {
VStack {
if tower.currentPage == .onboarding {
ContentViewA()
} else if tower.currentPage == .login {
ContentViewB()
}
}
}
}
And lastly, a content view.
struct ContentViewA: View {
#EnvironmentObject var tower: Tower
var body: some View {
Button(action: {
self.tower.currentPage = .login
}) {
Text("Go to Login")
}
}
}
}
In addition to New Devs greatly appreciated solution, I also used this article from BLCKBIRDS.
I'll expand on my comment. NavigationView/NavigationLink aren't the only ways to change views - a simple conditional can also be used to determine which view is rendered.
So, say, you have some class that contains the state of the login/onboarding information:
class AppState: ObservableObject {
enum UserFlow {
case onboarding, login, home
}
#Published var userFlow: UserFlow = .onboarding
// ...
}
Then your RootView could determine which user flow to show:
struct RootView: View {
#EnvironmentObject var appState: AppState
var body: some View {
if appState.userFlow == .onboarding {
OnboardingRootView()
} else if appState.userFlow == .login {
LoginRootView()
} else {
ContentView()
}
}
}
I am trying to rewrite my app using SwiftUI only and I am having difficulty with the EnvironmentObject, trying to understand how it works…
I want to redirect my app users to the appropriate page at launch, depending on:
if this is their first time
if they have a login,
if they want to start using without login
If it is the first time the app is launched, LocalStorage has no data so I present the app on a welcome page
I offer the choice of 2 buttons to click on:
“New User” which redirect to the main page of the app and create a new user
“Login” which present the login page to retrieve the last backup
If the app has previously been launched, I present the main page straight away.
Now said, if I initiate my “currentPage” as “MainView” or “LoginView”, it works - but NOT if it is set as “WelcomeView”.
I presume the problem comes when the variable gets changed from a subview? I thought the use of #EnvironmentObject was the way to get around this…
Can someone explain to me how it works?
My various files are:
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "WelcomeView" {
didSet {
objectWillChange.send(self)
}
}
}
import SwiftUI
struct ParentView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "WelcomeView" {
WelcomeView()
}
else if viewRouter.currentPage == "MainView" {
MainView()
}
else if viewRouter.currentPage == "LoginView" {
LoginView()
}
}
}
}
import SwiftUI
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons().environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
import SwiftUI
struct LoginButtons: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {
self.viewRouter.currentPage = "MainView"
}) {
Text("NEW USER")
}
Button(action: {
self.viewRouter.currentPage = "LoginView"
}) {
Text("I ALREADY HAVE AN ACCOUNT")
}
}
}
}
import SwiftUI
struct MainView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
import SwiftUI
struct LoginView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
Many Thanks in advance! :wink:
Ok so in your main view, the one that you are going to decide where to send your user, you could check for the app if it was lunched before or not, depending on that do whatever you want. Once you know how to do this, you can adapt to the other things. This is how you can check for it, again, in your main view router:
init() {
// Create initial Data if not data has been setup
if (InitialAppSetup().initialDataLoaded == false) {
InitialAppSetup().createInitialData()
}
// Onboarding screen
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "onboardingView"
} else {
currentPage = "homeView"
}
}
The InitialAppSetup() class has a UserDefault which goes like this:
#Published var initialDataLoaded: Bool = UserDefaults.standard.bool(forKey: "InitialData") {
didSet {
UserDefaults.standard.set(self.initialDataLoaded, forKey: "InitialData")
}
}
Ok... My 'mistake' was to add an extra ".environmentObject(ViewRouter())" when calling my subview "LoginButtons".
If I remove it, it works!.. But why?!?
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons()
// --> .environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
When debugging an issue with an app I am working on, I managed to shrink it down to this minimal example:
class RadioModel: ObservableObject {
#Published var selected: Int = 0
}
struct RadioButton: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel()
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton(idx: 0)
RadioButton(idx: 1)
RadioButton(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest()
}
}
}
}
This code looks pretty reasonable to me, although it exhibit a peculiar bug: when I hit the Refresh button, the radio buttons stop working. The radio buttons are not refreshed, and keep a reference to the old RadioModel instance, so when I click them they update that, and not the new one created after Refresh causes a new RadioListTest to be constructed. I suspect there is something wrong in the way I use EnvironmentObjects but I didn't find any reference suggesting that what I am doing is wrong. I know I could fix this particular problem in various ways that force a refresh in the radio buttons, but I would like to be able to understand which cases require a refresh forcing hack, I can't sprinkle the code with these just because "better safe than sorry", the performance is going to be hell if I have to redraw everything every time I make a modification.
edit: a clarification. The thing that is weird in my opinion and for which I would want an explanation, is this: why on refresh the RadioListTest is re-created (together with a new RadioModel) and its body re-evaluated but RadioButtons are created and the body properties are not evaluated, but the previous body is used. They both have only a view model as state, the same view model actually, but one have it as ObservedObject and the other as EnvironmentObject. I suspect it is a misuse of EnvironmentObject that I am doing, but I can't find any reference to why it is wrong
this works: (yes, i know, you know how to solve it, but i think this would be the "right" way.
problem is this line:
struct RadioListTest: View {
#ObservedObject var radioModel = RadioModel(). <<< problem
because the radioModel will be newly created each time the RadioListTest view is refreshed, so just create the instance one view above and it won't be created on every refresh (or do you want it to be created every time?!)
class RadioModel: ObservableObject {
#Published var selected: Int = 0
init() {
print("init radiomodel")
}
}
struct RadioButton<Content: View>: View {
let idx: Int
#EnvironmentObject var radioModel: RadioModel
var body: some View {
Button(action: {
self.radioModel.selected = self.idx
}, label: {
if radioModel.selected == idx {
Text("Button \(idx)").background(Color.yellow)
} else {
Text("Button \(idx)")
}
})
}
}
struct RadioListTest: View {
#EnvironmentObject var radioModel: RadioModel
var body: some View {
return VStack {
Text("You selected: \(radioModel.selected)")
RadioButton<Text>(idx: 0)
RadioButton<Text>(idx: 1)
RadioButton<Text>(idx: 2)
}.environmentObject(radioModel)
}
}
struct ContentView: View {
#ObservedObject var radioModel = RadioModel()
#State var refreshDate = Date()
func refresh() {
print("Refreshing...")
self.refreshDate = Date()
}
var body: some View {
VStack {
Text("\(refreshDate)")
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest().environmentObject(radioModel)
}
}
}
}
What is wrong with this piece of code?
Your RadioListTest subview is not updated on refresh() because it does not depend on changed parameter (refreshDate in this case), so SwiftUI rendering engine assume it is equal to previously created and does nothing with it:
HStack {
Button(action: {
self.refresh()
}, label: {
Text("Refresh")
})
RadioListTest() // << here !!
}
so the solution is to make this view dependent somehow on changed parameter, if it is required of course, and here fixed variant
RadioListTest().id(refreshDate)
I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}