SwiftUI AppDelegate: Show sheet when notification is tapped - ios

Problem
I want to show a sheet or navigate to a specific view with SwiftUI when the user presses on a notification.
I'm seeking advice on what the best approach would be to solve this problem in a SwiftUI app.
Inside my AppDelegate I currently have:
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
// Here I want to implement a function, that opens a sheet or navigates to a specific view //
print("User tapped notification")
completionHandler()
}
}
What would be the best way to do this?
Overview of structure of the app
#main
struct jsonlistingApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
MainView()
}
}
}
struct MainView: View {
var body: some View {
TabView {
PostList()
.tabItem {
Label("Søg", systemImage: "magnifyingglass")
}
FavoritesView()
.tabItem {
Label("Favoritter", systemImage: "heart")
}
if #available(iOS 15.0, *) {
BoligAgentView()
.tabItem {
Label("Boligagent", systemImage: "bell")
}
.badge(UIApplication.shared.applicationIconBadgeNumber)
}
else {
BoligAgentView()
.tabItem {
Label("Boligagent", systemImage: "bell")
}
}
Settings()
.tabItem {
Label("Profil", systemImage: "person")
}
}
}
Thanks in advance!

Related

How to open view on notification tapped using SwiftUI with proper hierarchy of views and go back to main application

How can I go to OrderReviewView with ContentViewz on navigation stack hierarchy? So navigation stack looks like ContentView -> OrderReviewView ?
At this moment I managed to open the view, but not in the way I expected. I managed to open the view in several ways, the first of which was using the internal notification NotificationCenter.default.publisher(for: NSNotification.Name("OrderReviewView")) . After receiving it in the ContentView class, I set the variable bool showReviewOrder. The view was opening but I couldn't roll back from the view to the Main class. Another way is presented below, the view also opens, but as in the first case, I can't go back to the main application class. I tried to add NavigationStack by changing the line let host = UIHostingController(rootView: NavigationStack {orderWriteScreen.navigationBarBackButtonHidden(true))} and NavigationLink to the OrderReviewScreen and structure as follow
NavigationStack {
ZStack {
VStack(alignment: .leading){
Text("Test")
}
NavigationLink(destination: ContentView())
{FAB() }
.padding(EdgeInsets(top: 0, leading: 0, bottom: 50, trailing: 20))
.frame(maxHeight: .infinity, alignment: .bottom)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
I was able to go back to the main application class, but there was a back button at the top of the screen and the hierarchy was still one window too many. I am attaching minimal working example of my code to show the problem.
#main
struct App: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
if authManager.authorized == nil {
SignInScreen()
} else {
Main()
}
}
}
}
struct Main: View {
var body: some View {
TabBar()
}
}
extension TabBar {
var baseTabBar: some View {
TabView(selection: $selection) {
OrderScreen()
.tag(Tab.contracts)
}
.padding(.bottom, _displayTabBar ? tabBarHeight : 0)
}
}
struct TabBar: View {
let tabBarHeight: CGFloat = 55
#State private var selection = Tab.farms
#State var _displayTabBar = true
init() {
UITabBar.appearance().isHidden = true
}
var body: some View {
ZStack(alignment: .bottom) {
baseTabBar
.environment(\.displayTabBar, displayTabBar)
if (_displayTabBar) {
styledTabBar
}
}
}
}
struct OrderScreen: View {
var body: some View {
ZStack {
VStack(alignment: .leading) {
}
}
}
struct OrderReviewScreen: View {
var body: some View {
VStack(alignment: .leading) {
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
FirebaseConfiguration.shared.setLoggerLevel(.debug)
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions) { _, _ in }
application.registerForRemoteNotifications()
Messaging.messaging().delegate = self
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Messaging.messaging().apnsToken = deviceToken
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
let info = userInfo as NSDictionary
guard let screen = info.value(forKey: "screen") as? String else { return }
guard let id = info.value(forKey: "id") as? String else { return }
notificationManager?.notifyId = id
notificationManager?.notifyScreen = screen
if screen == "order_details" {
let orderWriteScreen = OrderReviewScreen()
let host = UIHostingController(rootView: orderWriteScreen.navigationBarBackButtonHidden(true))
UIApplication.shared.windows.first?.rootViewController = host
}
completionHandler()
}
}
}

How to use if/ForEach in a SwiftUI View to show IAP modal?

I'm trying to display a Subscribe Now modal view instantly after the app starts to encourage users to subscribe to the Pro In-App Purchase, so I used the .onAppear modifier, and it works fine only if I want to show the modal every time the app starts.
struct ContentView: View {
#State private var selection: String? = nil
#State private var showModal = false
#ObservedObject var storeManager: StoreManager
var body: some View {
NavigationView {
VStack {
// Contents Here
}
}
.onAppear {
self.selection = "Pro"
self.showModal.toggle()
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Pro(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Now, the problem begins when I want to display the modal only to those who have not subscribed yet to the Pro IAP, so I modified .onAppear to:
.onAppear {
ForEach(storeManager.myProducts, id: \.self) { product in
VStack {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
}
But, the if and ForEach seems not to work smoothly with structs and views. How should I use them in my case?
Update:
Based on the answers, I have changed the loop inside .onAppear to make the code conforms to SwiftUI requirements:
.onAppear {
storeManager.myProducts.forEach { product in
// Alternatively, I can use (for in) loop:
// for product in storeManager.myProducts {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
Now, errors have gone away but the modal is not displayed on startup.
I discovered that the problem is, storeManager.myProducts is not loaded in .onAppear modifier, while it's loaded correctly when I put the same loop in a button instead of .onAppear, any ideas? Why does onAppear doesn't load the IAP? Where should I put the code to make the modal run when the view loaded?
Update 2:
Here is a Minimal Reproducible Example:
App:
import SwiftUI
#main
struct Reprod_SOFApp: App {
#StateObject var storeManager = StoreManager()
let productIDs = ["xxxxxxxxxxxxxxxxxxxxx"]
var body: some Scene {
DocumentGroup(newDocument: Reprod_SOFDocument()) { file in
ContentView(document: file.$document, storeManager: storeManager)
.onAppear() {
storeManager.getProducts(productIDs: productIDs)
}
}
}
}
ContentView:
import SwiftUI
import StoreKit
struct ContentView: View {
#Binding var document: Reprod_SOFDocument
#State private var selection: String? = nil
#State private var showModal = false
#ObservedObject var storeManager: StoreManager
var test = ["t"]
var body: some View {
TextEditor(text: $document.text)
.onAppear {
// storeManager.myProducts.forEach(id: \.self) { product in
// Alternatively, I can use (for in) loop:
for i in test {
if !i.isEmpty {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Modal(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Modal:
import SwiftUI
import StoreKit
struct Modal: View {
#Binding var showModal: Bool
#ObservedObject var storeManager: StoreManager
var body: some View {
Text("hello world")
}
}
StoreManager:
import Foundation
import StoreKit
class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#Published var myProducts = [SKProduct]()
var request: SKProductsRequest!
#Published var transactionState: SKPaymentTransactionState?
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
transactionState = .purchasing
case .purchased:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .purchased
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .restored
case .failed, .deferred:
print("Payment Queue Error: \(String(describing: transaction.error))")
queue.finishTransaction(transaction)
transactionState = .failed
default:
queue.finishTransaction(transaction)
}
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.myProducts.append(fetchedProduct)
}
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: \(invalidIdentifier)")
}
}
func getProducts(productIDs: [String]) {
print("Start requesting products ...")
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: \(error)")
}
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
func restoreProducts() {
print("Restoring products ...")
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
Here is a link to Minimal Reproducible Example
Instead of using .onAppear modifier to display the modal, you can change the initial values of selection and showModal:
#State private var selection: String? = "Pro"
#State private var showModal = !UserDefaults.standard.bool(forKey: "xxxxxxxxxxxxxxxxxxxxx") ? true : false
// Write your product identifier instead of "xxxxxxxxxxxxxxxxxxxxx"
This way, modal view will be shown instantly after the content view loads.
Note: For showModal, I've applied a conditional if instead of simply true, since you said you want to show the modal only to those who have not subscribed yet to the Pro IAP.
I would recommend to separate logic into a viewmodel, and you only need to manage one identified object to show your pro modal.
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
Text("Hello")
.onAppear(perform: viewModel.fetchStatus)
.sheet(item: $viewModel.carrier) { carrier in
ModalView(storeManager: carrier.storeManager)
}
}
}
class ContentViewModel: ObservableObject {
#Published var carrier: ModalObject?
private let storeManager: StoreManager
func fetchStatus() {
// do something asynchronous like
storeManager.fetchProducts() { [self] products in
if !products.contains(proProducts) {
self.carrier = ModalObject(storeManager: self.storeManager)
}
}
}
}
struct ModalObject: Identifiable {
var id = UUID()
let storeManager: StoreManager
}
I just wrote without compiling, please check with your xcode.
Your .onAppear{} should be Swift code instead SwiftUI (ForEach, VStack). VStack are view structs.

SwiftUI .onAppear, only running once

In a SwiftUI app I have code like this:
var body: some View {
VStack {
Spacer()
........
}
.onAppear {
.... I want to have some code here ....
.... to run when the view appears ....
}
}
My problem is that I would like to run some code inside the .onAppear block, so that it gets run when the app appears on screen, after launching or after being in the background for a while. But it seems like this code is only run once at app launch, and never after. Am I missing something? Or should I use a different strategy to get the result I want?
If you're linking against iOS14 then you can take advantage of the new scenePhase concept:
#Environment(\.scenePhase) var scenePhase
Where ever you are the Environment if injecting this property that you can test against three conditions:
switch newPhase {
case .inactive:
print("inactive")
case .active:
print("active")
case .background:
print("background")
}
So, all together:
struct ContentView: View {
#Environment(\.scenePhase) var scenePhase
var body: some View {
Text("Hello, World!")
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .inactive:
print("inactive")
case .active:
print("active")
case .background:
print("background")
}
}
}
}
You would have to observe the event when the app is entering foreground and publish it using #Published to the ContentView. Here's how:
struct ContentView: View {
#ObservedObject var observer = Observer()
var body: some View {
VStack {
Spacer()
//...
}
.onReceive(self.observer.$enteredForeground) { _ in
print("App entered foreground!") // do stuff here
}
}
}
class Observer: ObservableObject {
#Published var enteredForeground = true
init() {
if #available(iOS 13.0, *) {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIScene.willEnterForegroundNotification, object: nil)
} else {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
}
#objc func willEnterForeground() {
enteredForeground.toggle()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}

How can I open a specific View in SwiftUI when a user opens a notification?

First, how would I be able to check if a user has opened a notification? (in all cases, when phone is locked, when app is in foreground, and when app is in background)
If the phone is locked, or the app hasn't been started, is it as simple as checking launch options in application() in AppDelegate? Ex:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let notificationOption = launchOptions?[.remoteNotification]
}
For the other two cases (foreground & background), what functions would be able to check in those cases? And would they still go in AppDelegate?
Second, when I do set up functions to check if the app has been opened from a notification, how would I be able to navigate SwiftUI to a specific view? In this case, it would be simply a specific TabView Tab.
I currently initialize my tabViewModel in SceneDelegate, and set the default current tab there. For example, currentTab = 2.
TabView(selection: $tabViewModel.currentTab) {
...
}
This worked well for deep navigation after the user tapped a chat notification. The chat notification comes from Firebase so I have to retrieve the ChatID from the notification to know which one I have to open.
With that in mind, I also know that the selected tab on the tabBar for the messages is the Int value of 1.
Now I can create a manager that works as a observed singelton object that listens to each notification that a user has tapped.
Above in AppDelegate:
weak var notificationManager: NotificationManager?
User notification tapped:
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID from userNotificationCenter didReceive: \(messageID)")
}
print("*** NotificationInfo: \(userInfo) ***")
let info = userInfo as NSDictionary
guard let chatID = info.value(forKey: "chatID") as? String else { return } // retrieving the ChatID from notification payload
// Navigate to the room of the chatID when chat notification is tapped
notificationManager?.pageToNavigationTo = 1 // navigate to messages view
notificationManager?.recievedNotificationFromChatID = chatID // navigate to correct chat
completionHandler()
}
// MARK: - SwiftUI Lifecycle
#main
struct VitaliiApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var session = SessionStore()
let notificationManager = NotificationManager()
func setUpdNotificationManager() {
appDelegate.notificationManager = notificationManager
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(session)
.environmentObject(notificationManager)
.onAppear {
setUpdNotificationManager()
}
}
}
}
The NotificationManager:
class NotificationManager: ObservableObject {
static let shared = NotificationManager()
#Published var pageToNavigationTo : Int?
#Published var recievedNotificationFromChatID: String?
}
My ContentView with onRecieve() that listens to the NotificationManager and navigates to my InboxMessageView if a notification is tapped.
struct ContentView: View {
#State var selection: Int = 0
#EnvironmentObject var session: SessionStore
#State var activeChatID: String?
let tabBarImageNames = ["person.3", "message", "person"]
#EnvironmentObject private var notificationManager: NotificationManager
var body: some View {
ZStack {
Color.black
.ignoresSafeArea()
VStack {
ZStack {
switch selection {
case 0:
NavigationView {
HomeView()
}
case 1:
NavigationView {
InboxMessagesView(user: session.userSession, activeChatID: $activeChatID)
}
.accentColor(.white)
default:
NavigationView {
ProfileView(session: session.userSession)
}
}
}
Spacer()
HStack {
ForEach(0..<3) { num in
Button {
selection = num
} label: {
Spacer()
Image(systemName: tabBarImageNames[num])
.padding(.top, 10)
.font(.system(size: 20, weight: .medium))
.foregroundColor(selection == num ? .red : .white.opacity(0.7))
Spacer()
}
}
}
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.onReceive(notificationManager.$pageToNavigationTo) {
guard let notificationSelection = $0 else { return }
self.selection = notificationSelection // navigates to page InboxMessagesView
}
.onReceive(notificationManager.$recievedNotificationFromChatID) {
guard $0 != nil else { return }
self.activeChatID = $0 // navigates to the correct chat that is associated with the chatID when the user taps on a chat notification
}
}
}
In the InboxMessagesView I just have a ForEach loop with all the current user chats in a NavigationLink where each link is tagged with their corresponding chatID. It navigates by itself to the right chat if the notification is tapped. I can add the code for that View too if you want to.
When app is foreground or background userNotificationCenter with UNUserNotificationCenterDelegate will be called. When app is not started, you can check notification with SceneDelegate scene connectionOptions.
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
print("open notification")
let userInfo = response.notification.request.content.userInfo
print(userInfo)
completionHandler()
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let notificationResponse = connectionOptions.notificationResponse {
// you can get notification here
}
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
[UPDATE]
you don't need SceneDelegate scene connectionOptions. even when app is not started. userNotificationCenter supports when app is foreground, background, and app is not started.
https://stackoverflow.com/a/60857140/12208004

Completely move to other view and don't allow to go back in SwiftUI

I'm developing a simple iOS app with SwiftUI with two views: a LogInView() and a HomeView().
What I want is really simple: when the user clicks on the Login Button on LogInView() I want the app to hide the LogInView() and show the HomeView(), full screen, not like a modal and without allowing the user to go back.
This could be easily done with Storyboards in Swift and UIKit, is there any way to do this with SwiftUI?
Any help is appreciated.
Thanks in advance.
My code:
LogInView:
struct LogInView: View {
var body: some View {
VStack {
Text("Welcome to Mamoot!")
.font(.largeTitle)
.fontWeight(.heavy)
Text("We are glad to have you here.")
Text("Please log in with your Mastodon or Twitter account to continue.")
.multilineTextAlignment(.center)
.lineLimit(4)
.padding()
Spacer()
FloatingTextField(title: "Username", placeholder: "Username", width: 300, type: "Username")
FloatingTextField(title: "Password", placeholder: "Password", width: 300, type: "password")
.padding(.top, -50)
Spacer()
ZStack {
Button(action: { /* go to HomeView() */ }) {
Text("Log in")
.foregroundColor(Color.white)
.bold()
.shadow(color: .red, radius: 10)
}
.padding(.leading, 140)
.padding(.trailing, 140)
.padding(.top, 15)
.padding(.bottom, 15)
.background(Color.red)
.cornerRadius(10)
}
.padding(.bottom)
}
}
}
HomeView:
struct HomeView: View {
var body: some View {
Text("Home Page")
}
}
Update: I got some time to update the answer, and add a transition. Note that I changed Group by VStack, otherwise the transition does not work.
You can alter the duration in the withAnimationcall (button closure).
I also moved some modifiers in your button, so the whole thing is "tappable". Otherwise, only tapping on the text of the button would trigger the action.
You can use an ObservedObject, an EnvironmentObject or a Binding. Here's an example with ObservedObject:
import SwiftUI
class Model: ObservableObject {
#Published var loggedIn = false
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
if model.loggedIn {
HomeView().transition(.opacity)
} else {
LogInView(model: model).transition(.opacity)
}
}
}
}
struct HomeView: View {
var body: some View {
Text("Home Page")
}
}
struct LogInView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("Welcome to Mamoot!")
.font(.largeTitle)
.fontWeight(.heavy)
Text("We are glad to have you here.")
Text("Please log in with your Mastodon or Twitter account to continue.")
.multilineTextAlignment(.center)
.lineLimit(4)
.padding()
Spacer()
// FloatingTextField(title: "Username", placeholder: "Username", width: 300, type: "Username")
// FloatingTextField(title: "Password", placeholder: "Password", width: 300, type: "password")
// .padding(.top, -50)
Spacer()
ZStack {
Button(action: {
withAnimation(.easeInOut(duration: 1.0)) {
self.model.loggedIn = true
}
}) {
Text("Log in")
.foregroundColor(Color.white)
.bold()
.shadow(color: .red, radius: 10)
// moved modifiers here, so the whole button is tappable
.padding(.leading, 140)
.padding(.trailing, 140)
.padding(.top, 15)
.padding(.bottom, 15)
.background(Color.red)
.cornerRadius(10)
}
}
.padding(.bottom)
}
}
}
The answer by #kontiki is probably the most SwiftUI-y, but I will present a different solution, probably not as good! But maybe more flexible/scalable.
You can swap rootView of UIHostingController:
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
fileprivate lazy var appCoordinator: AppCoordinator = {
let rootViewController: UIHostingController<AnyView> = .init(rootView: EmptyView().eraseToAny())
window?.rootViewController = rootViewController
let navigationHandler: (AnyScreen, TransitionAnimation) -> Void = { [unowned rootViewController, window] (newRootScreen: AnyScreen, transitionAnimation: TransitionAnimation) in
UIView.transition(
with: window!,
duration: 0.5,
options: transitionAnimation.asUIKitTransitionAnimation,
animations: { rootViewController.rootView = newRootScreen },
completion: nil
)
}
return AppCoordinator(
dependencies: (
securePersistence: KeyValueStore(KeychainSwift()),
preferences: .default
),
navigator: navigationHandler
)
}()
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
self.window = .fromScene(scene)
appCoordinator.start()
}
}
enum TransitionAnimation {
case flipFromLeft
case flipFromRight
}
private extension TransitionAnimation {
var asUIKitTransitionAnimation: UIView.AnimationOptions {
switch self {
case .flipFromLeft: return UIView.AnimationOptions.transitionFlipFromLeft
case .flipFromRight: return UIView.AnimationOptions.transitionFlipFromRight
}
}
}
AppCoordinator
And here is the AppCoordinator:
final class AppCoordinator {
private let preferences: Preferences
private let securePersistence: SecurePersistence
private let navigationHandler: (AnyScreen, TransitionAnimation) -> Void
init(
dependencies: (securePersistence: SecurePersistence, preferences: Preferences),
navigator navigationHandler: #escaping (AnyScreen, TransitionAnimation) -> Void
) {
self.preferences = dependencies.preferences
self.securePersistence = dependencies.securePersistence
self.navigationHandler = navigationHandler
}
}
// MARK: Internal
internal extension AppCoordinator {
func start() {
navigate(to: initialDestination)
}
}
// MARK: Destination
private extension AppCoordinator {
enum Destination {
case welcome, getStarted, main
}
func navigate(to destination: Destination, transitionAnimation: TransitionAnimation = .flipFromLeft) {
let screen = screenForDestination(destination)
navigationHandler(screen, transitionAnimation)
}
func screenForDestination(_ destination: Destination) -> AnyScreen {
switch destination {
case .welcome: return AnyScreen(welcome)
case .getStarted: return AnyScreen(getStarted)
case .main: return AnyScreen(main)
}
}
var initialDestination: Destination {
guard preferences.hasAgreedToTermsAndPolicy else {
return .welcome
}
guard securePersistence.isAccountSetup else {
return .getStarted
}
return .main
}
}
// MARK: - Screens
private extension AppCoordinator {
var welcome: some Screen {
WelcomeScreen()
.environmentObject(
WelcomeViewModel(
preferences: preferences,
termsHaveBeenAccepted: { [unowned self] in self.start() }
)
)
}
var getStarted: some Screen {
GetStartedScreen()
.environmentObject(
GetStartedViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletCreated: { [unowned self] in self.navigate(to: .main) }
)
)
}
var main: some Screen {
return MainScreen().environmentObject(
MainViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletDeleted: { [unowned self] in
self.navigate(to: .getStarted, transitionAnimation: .flipFromRight)
}
)
)
}
}

Resources