How do I start my SwiftUI app in a children view? - swiftui

Okay, here's what my app looks like.
#main
struct MemorizableApp: App {
init() {
FirebaseApp.configure() // Configure the FirebaseApp instance
Database.database().isPersistenceEnabled = true // Set DB persistence to true
}
var body: some Scene {
WindowGroup {
if Auth.auth().currentUser == nil {
SignInView()
.onOpenURL{ url in
GIDSignIn.sharedInstance.handle(url)
}
} else {
MainView()
}
}
}
}
And this is MemorizableApp.swift. As you can see, the app starts in MainView. But I want the app to launch in NotebooksView instead. Obviously, changing MainView to NotebooksView in the code above won't work because 1) NotebooksView doesn't have a NavigationView in it, and 2) I won't be able to navigate back to MainView. But somehow Apple's Shortcuts app did exactly this without creating a custom back button. How can I achieve this behaviour?

What you need to do is activate your NavigationLink programmatically:
In MainView add: #State var shouldNavigate = false, then modify your NavigationLink to the following:
NavigationLink(destination: NoteBooksView(), isActive: $shouldNavigate) {
...
}
And in MemorizableApp:
#main
struct MemorizableApp: App {
init() {
FirebaseApp.configure() // Configure the FirebaseApp instance
Database.database().isPersistenceEnabled = true // Set DB persistence to true
}
var body: some Scene {
WindowGroup {
if Auth.auth().currentUser == nil {
SignInView()
.onOpenURL{ url in
GIDSignIn.sharedInstance.handle(url)
}
} else {
MainView(shouldNavigate: true) //set it to true
}
}
}
}
Note: The use of NavigationLink(destination: Content, isActive: Binding<Bool>) & NavigationView is deprecated in iOS 16. Take a look here & here.

Related

How to use NavigationLink for List view swipe action

I'm wondering how to place NavigationLink into swipeActions section in code below. Code itself is compiled without any issue but when I tap "Edit" link nothing happens. My intention is to show another view by tapping "Edit".
Thanks
var body: some View {
List {
ForEach(processes, id: \.id) { process in
NavigationLink(process.name!, destination: MeasurementsView(procID: process.id!, procName: process.name!))
.swipeActions() {
Button("Delete") {
deleteProcess = true
}.tint(.red)
NavigationLink("Edit", destination: ProcessView(procID: process.id!, procName: process.name!)).tint(.blue)
}
}
}
}
It does not work because swipeActions context is out of NavigationView. Instead we can use same NavigationLink for conditional navigation, depending on action.
Here is a simplified demo of possible approach - make destination conditional and use programmatic activation of link.
Tested with Xcode 13.2 / iOS 15.2
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<2, id: \.id) {
// separate into standalone view for better
// state management
ProcessRowView(process: $0)
}
}
}
}
}
struct ProcessRowView: View {
enum Action {
case view
case edit
}
#State private var isActive = false
#State private var action: Action?
let process: Int
var body: some View {
// by default navigate as-is
NavigationLink("Item: \(process)", destination: destination, isActive: $isActive)
.swipeActions() {
Button("Delete") {
}.tint(.red)
Button("Edit") {
action = .edit // specific action
isActive = true // activate link programmatically
}.tint(.blue)
}
.onChange(of: isActive) {
if !$0 {
action = nil // reset back
}
}
}
#ViewBuilder
private var destination: some View {
// construct destination depending on action
if case .edit = action {
Text("ProcessView")
} else {
// just to demo different type destinations
Color.yellow.overlay(Text("MeasurementsView"))
}
}
}

How to fire `onAppear` method when a view comes back from `App Settings` changing a notifications setting?

I'm currently developing an application using SwiftUI.
I'm trying to make an app using Local Notification.
I want to reflect on a set of Allow Notifications in an App Settings to some views.
But when a view comes back from the App Settings using a navigation link I attached(not back button), the onAppear method doesn't fire and I can't show a collect value...
In my codes:
1.Tap Open Settings to navigate to an App Setting
2.Change a Notifications setting
3.Tap NotificationTest button
4.Come back to ContentView then onAppear method doesn't fire
How could I solve this problem?
Here are the codes:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var isNotification = false
var body: some View {
VStack{
Text(isNotification ? "STATUS:ON" : "STATUS:OFF")
.padding()
Text("Open Settings")
.padding()
.onTapGesture {
UIApplication.shared.open(URL(string:UIApplication.openSettingsURLString)!)
}
}
.onAppear(){
confirmNotification()
}
}
func confirmNotification(){
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound]){
success, error in
if success{
isNotification = true
print("Notification set")
}else{
isNotification = false
}
}
}
}
NotificationTestApp.swift
import SwiftUI
#main
struct NotificationTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
View has already appeared, just application is in background. Try to use instead (or additionally) scenePhase
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
#State var isNotification = false
var body: some View {
VStack{
Text(isNotification ? "STATUS:ON" : "STATUS:OFF")
.padding()
Text("Open Settings")
.padding()
.onTapGesture {
UIApplication.shared.open(URL(string:UIApplication.openSettingsURLString)!)
}
}
.onAppear(){
confirmNotification()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
confirmNotification()
default:
break
}
}
}

Where do I handle the initial value before the first onChange(of:)?

I want to load some data when my app is first launched, and when the app is foregrounded I want to ensure I have the latest data.
The state is stored in a ViewModel class, which my view owns as a #StateObject. I read the ScenePhase from the Environment, and in onChange(of: scenePhase), I call a method on my ViewModel to start the reload if needed.
But when should I start the initial load?
ContentView.init is too early, because scenePhase is .background. And even if it were .active, I'm apparently not supposed to access StateObject from init — SwiftUI logs a runtime warning.
ViewModel.init is too early as well — theoretically, I think the view model could be created even if the app were never brought to the foreground.
The first time var body is accessed, the scenePhase is .active. onChange(of:) doesn't call the closure for its initial value, so it's never called until I background and re-foreground the app.
class ViewModel: ObservableObject {
init() {
// 1. reload() here? Could happen without the app entering the foreground.
}
func reload() { ... }
}
struct ContentView: View {
#Environment(\.scenePhase) var scenePhase
#StateObject var viewModel = ViewModel()
init() {
// 2. viewModel.reload() here?
// - problem 1: scenePhase == .background, not .active
// - problem 2: not supposed to access a #StateObject here anyway
}
var body: some View {
(...)
// The initial render happens when scenePhase == .active,
// so I don't get the onChange callback until it changes again.
.onChange(of: scenePhase) {
if $0 == .active {
viewModel.reload()
}
}
}
}
If your ContentView is the root View you can just use onAppear.
However, if your ContentView can disappear and then reappear the above solution will not work.
A possible solution may be to inject a variable into the Environment:
struct LaunchAppKey: EnvironmentKey {
static let defaultValue = Binding.constant(false)
}
extension EnvironmentValues {
var isAppLaunched: Binding<Bool> {
get { return self[LaunchAppKey] }
set { self[LaunchAppKey] = newValue }
}
}
#main
struct TestApp: App {
#State private var isAppLaunched = false
#State private var showContentView = true
var body: some Scene {
WindowGroup {
VStack {
if showContentView {
ContentView()
} else {
Text("Some other view")
}
}
.environment(\.isAppLaunched, $isAppLaunched)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
showContentView.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
showContentView.toggle()
}
}
}
}
}
struct ContentView: View {
#Environment(\.isAppLaunched) var isAppLaunched
var body: some View {
(...)
.onAppear {
guard !isAppLaunched.wrappedValue else { return }
isAppLaunched.wrappedValue = true
print("apppear")
}
}
}

How can I show a page depending of child button clicked with SwiftUI?

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] }
}
}
}

Disable drag to dismiss in SwiftUI Modal

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)
}
}
}