Using ObservableObject to navigate to different views using SwiftUI - swift

I'm trying to create logic that will send the user to the correct view once they have entered their credentials. In the login page, when the user logs in they get a token that can be used to call an API. I store that token in KeychainSwift. I then can check to see if the field where I stored it has an entry that is not null and not empty to route the user to the main page. I'm using logic like this and this to route me to the correct view.
My content view code is as follows:
struct ContentView: View {
#ObservedObject var userAuth = UserAuth()
var body: some View {
if(self.userAuth.isLoggedIn){
return AnyView(MainPageView())
}else{
return AnyView(AccountPageView())
}
}
}
UserAuth is as follows:
import Combine
class UserAuth: ObservableObject {
// let objectWillChange = ObservableObjectPublisher()
#Published var isLoggedIn: Bool = false;
init() {
let keychain = KeychainSwift()
let userToken = keychain.get("userToken") ?? ""
if(!userToken.isEmpty || userToken != "" ){
self.isLoggedIn = true
}else{
self.isLoggedIn = false
}
}
}
The overall architecture im trying to achieve is this:
The issue is that there is no direct way to route from a view to another hence why I resorted to using logic that that displayed in ContentView. I have similar logic for login. I have a view (loginformview) that either routes you to the login form view or to the main page view. But once you login in the login form I would need to go back to the previous view (loginformview) where the check exist to see if I should return the login page or go to the main page(this time sending you to the main page). It seems very cyclical, anyone had any idea to proper tackle logic like this ?

Related

Remember #State variable after switching views

I'm making an application for macOS with SwiftUI. I have a button that starts a download process asynchronously by calling Task.init and an async function. When the task starts, I set a #State busy variable to true so I can display a ProgressView while the download is happening. The problem is if I switch to a different view then back while it is downloading, the state gets reset although the task still runs. How can I make it so it remembers the state, or maybe check if the task is still running?
Here is a stripped down example:
import SwiftUI
struct DownloadRow: View {
let content: Content
#State var busy = false
var body: some View {
if !busy {
Button() {
Task.init {
busy = true
await content.download()
busy = false
}
} label: {
Label("Download")
}
} else {
ProgressView()
}
}
}
You could put the variable that tracks the download's progress into an ObservableObject class and make the progress variable #Published. Have the object that tracks your progress as an #ObservedObject in the view. By doing so, you decouple the progress tracker from the view's lifecycle. Make sure this view does not initialize the progress tracker, or a new object will be built when the view is built again.

How can I open URLs in watchOS?

Can a standalone Apple Watch app open a URL like we can in iOS:
UIApplication.sharedApplication().openURL(url!)
I want to display a message about an update when a new app is published. I want the user to go the URL for the update. Is there any good way?
The following answer works in watchOS 6.2 and above. It initiates a web authentication session, which opens a web view, similar to WKWebView on iOS. You can even swipe from the left to go back.
Keep in mind that it doesn't save cookies, so, for example, if you log in to a website and close it, you have to log in again when you open it back up.
import AuthenticationServices
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Tap me") {
guard let url = URL(string: "https://example.com") else {
return
}
// Source: https://www.reddit.com/r/apple/comments/rcn2h7/comment/hnwr8do/
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: nil
) { _, _ in
}
// Makes the "Watch App Wants To Use example.com to Sign In" popup not show up
session.prefersEphemeralWebBrowserSession = true
session.start()
}
}
}
I don't know if you're using SwiftUI or not, but the Link View is shown in the Documentation as available for watchOS. However using it in practice only prompts the user to open the link on their iPhone. As a standalone app since Safari is not available on watchOS you might want to consider other options to alert your user of an update.
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
Link("Apple.com", destination: URL(string: "https://www.apple.com")!)
}
}
}

SwiftUI: Admob Interstitial Ad is not being presented on rootViewController

CODED IN SWIFTUI
I have implemented a settings page within my SwiftUI app that is presented upon clicking the button within the view. This view is wrapped within a sheet. You can see the code below:
TabsBar(showOptionsTab: $showOptionsTab)
.sheet(isPresented: $showOptionsTab)
{
OptionsTab(showOptionsTab: self.$showOptionsTab)
}
I have implemented an interstitial within this "OptionsTab" that is created when the options tab is loaded within the .onAppear() call and presented when the back button is selected. You can the code from the OptionsTab below:
#State private var interstitial : GADInterstitial!
VStack
{
... other code
Button(action:
{
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
self.showOptionsTab.toggle()
})
{
Image(systemName: "xmark")
.font(.largeTitle)
}
}
.onAppear
{
self.interstitial = GADInterstitial(adUnitID: "addID")
let req = GADRequest()
self.interstitial.load(req)
}
I have additional code within the button that logs the status of the interstitial, and it shows it as being READY, and the present code is being hit... but it never presents itself.
I have implemented this exact code in the ContentView, and the interstitial loads perfectly fine. Something about this code being within the sheet is causing the interstitial not to present.
Is the rootViewController the incorrect view to load the interstitial on when in a view that is presented from a sheet? Otherwise, what else am I doing wrong?
Thanks for the help in advance!
Try this
TabsBar(showOptionsTab: $showOptionsTab)
.sheet(isPresented: $showOptionsTab)
{
OptionsTab(showOptionsTab: self.$showOptionsTab)
.OnDisapear
{
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
}
}
And then add the state interstitial variable within this view, and onAppear of the ContextView, create the interstitial there.
Solution 2:
(not tested but should work):
present on the presentingViewController instead of the rootViewController
if ( self.interstitial.isReady)
{
let root = UIApplication.shared.windows.first?.rootViewController?.presentingViewController
self.interstitial.present(fromRootViewController: root!)
}
check the reason, at the end of the answer
Solution 1:
it sounds like that there's something that prevents Interstitial ad to be presented when a sheet view is already presented
in my case i wanted to present the ad immediately after the sheet view is presented, which didn't work
what worked is, Presenting the ad, and after it being dismissed, the sheet view will be presented , and it worked!
for the Interstitial i used this code
import SwiftUI
import GoogleMobileAds
import UIKit
final class Interstitial:NSObject, GADInterstitialDelegate{
var interstitialID = "ca-app-pub-3940256099942544/4411468910"
var interstitial:GADInterstitial = GADInterstitial(adUnitID: "ca-app-pub-3940256099942544/4411468910")
var sheet: (() -> Void)? = nil //this closure will be executed after dismissing the ad
override init() {
super.init()
LoadInterstitial()
}
func LoadInterstitial(){
let req = GADRequest()
self.interstitial.load(req)
self.interstitial.delegate = self
}
func showAd(then sheet:(() -> Void)?){
if self.interstitial.isReady{
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
self.sheet = sheet
}
else{
print("Not Ready")
}
}
func interstitialDidDismissScreen(_ ad: GADInterstitial) {
self.interstitial = GADInterstitial(adUnitID: interstitialID)
LoadInterstitial()
if let presentSheet = sheet {
presentSheet()
}
}
}
here we pass whatever we want to happen to the closure which will be called when interstitialDidDismissScreen is called
now instead of switching the state when button is pressed, it will be switched after the ad is dimissed
at the top of your contentView:
struct HistoryView: View {
private var fullScreenAd: Interstitial!
init() {
fullScreenAd = Interstitial()
}
}
then in your button:
Button(action: {
self.interstitial.showAd {
self.showOptionsTab.toggle()
}
})
Note: i used the code of interstitial Ad from this gist
Also, AFAIK we can't tell if it's an bug in swiftUI, or it's something googleAdmob was supposed to take care of ?, there's no SwiftUI support From google so we can't blame them.
the problem AFAIK, that in iOS 13, grabbing the root ViewController will return the View Controller that presented the sheet, and when trying to present the ad, UIKit will complain that you are trying to present A view controller while a view controller is already present(the sheet),
Solution ? simple, present the Ad, on the presented view Controller(The sheet)

Sharing ViewModel between SwiftUI components

I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.

Refreshing a view controller on Firebase logout

I have a view controller that has a UICollectionView with information pulled from Firebase. When a user visits this view controller their info is placed in the collection view cells.
However, when the user logs out and logs in with a different account, the previous user's data stays in the collection view rather than re-populating with the new user's info.
Right now everything is called from viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
loadData()
loadOngoingQuest()
collectionView.dataSource = self
collectionView.delegate = self
}
loadData() and loadOngoingQuest() pull info from Firebase into arrays used to populate the collection view.
I have tried adding everything in viewWillAppear() instead, but it causes some glitches as the collection view involves scrolling and other animations that I don't want to run every time the view controller appears.
Basically I just want to reload the entire view controller every time the user logs out. How can I do this?
You should do something like this, see comments for more info:
let firebaseAuth = FIRAuth.auth()
do {
try firebaseAuth?.signOut()
// clear the array that your collectionView is based on
// reload your collectionView (which will clear all the data since your array is empty)
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
Update:
Another way of handling this:
Create a Struct with the following property:
struct Global {
var isNewUser = false
}
Whenever you logout a user do this:
Global.isNewUser = true
And in your collectionView you check if Global.isNewUser == true then reload everything and most important do set Global.isNewUser = false. That way you´ll only load everything once.
After a lot of frustration I figured out the issue!
It was nothing to do with refreshing collection views or view controllers. I was using Firebase refs wrong. I had saved two refs in as global constants in a struct in hopes of not having to declare them on every VC. It was like this:
struct GlobalConstants {
struct Refs {
static let userRef = AppDatabaseReference.users(uid: (Auth.auth().currentUser?.uid)!).reference()
static let currentUser = Auth.auth().currentUser?.uid
}
}
I would then use them anywhere in my code like this:
var questsRef = var questsRef = GlobalConstants.Refs.userRef.reference().child("quests")
This would cause issues unless I terminated the app and opened it again, refreshing these variables.
I went back to declaring let userRef = AppDatabaseReference.users(uid: (Auth.auth().currentUser?.uid)!).reference() at the top in every VC and it solved the issue!