Popping views using SceneDelegate.toView() - swiftui

I have a UI where you can navigate/push views like this:
AView -> BView -> CView -> DView
I want to pop a couple of views (get to BView from DView) instead of placing a new view (BView) on top of existing stack and I found the way to do this:
(UIApplication.shared.connectedScenes.first?.delegate as?
SceneDelegate)?.toBView()
What I don't understand is why does it call CView's init() when I try to return from DView to BView?
Here's the output i get in debug console:
AView init
BView init
CView init
DView init
contract.
button pressed
BView init
**CView init**
Why does it call CView's init() and how to avoid this behaviour?
AView.swift:
import SwiftUI
struct AView: View {
init() {
print("AView init")
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
init() {
print("BView init")
}
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
init() {
print("CView init")
}
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
init() {
print("DView init")
}
var body: some View {
Button(action: {
print("button pressed")
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toBView()
},
label: {
Text("Back!")
})
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let aView = AView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: AView)
self.window = window
window.makeKeyAndVisible()
}
}
func toBView() {
let bView = BView()
window?.rootViewController = UIHostingController(rootView: bView)
}
}

This behavior is not wrong. If you start your app and display AView you will see in the console that it prints init from A and B.
It's because BView() is already initialized in your AView, even though you haven't activated the NavigationLink.
NavigationLink(destination: BView()) {
So this behavior is not specific to your pop back action, happens already at the beginning. See this solution from Asperi regarding multiple init() aswell, if you are concerned about calling init() more than once

Related

NavigationLink is not clickable after returning to the view from it's child view

My app has a multilevel layout
AView -> BView -> CView -> DView.
I change
window.rootViewController
to BView in order to "pop" 2 top views but for some reason when i come back to BView it's NavigationLink is not clickable.
Any ideas on how to fix this? It seems like BView doesn't know that it became visible..
AView.swift:
import SwiftUI
struct AView: View {
init() {
print("AView init")
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
init() {
print("BView init")
}
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
init() {
print("CView init")
}
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
init() {
print("DView init")
}
var body: some View {
Button(action: {
print("button pressed")
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toBView()
},
label: {
Text("Back!")
})
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let aView = AView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: aView)
self.window = window
window.makeKeyAndVisible()
}
}
func toBView() {
let bView = BView()
window?.rootViewController = UIHostingController(rootView: bView)
}
}
Asperi already stated the problem.. you are setting BView as root level. Actually changing that, shouldn't be a big problem.
But back to your question why does the NavigationLink won't work anymore. Thats because ViewA, which contains the NavigationView is not in the RootLevel anymore. Hence you will need to provide a new NavigationView, but only and only when BView is the root view.
So inside BView, add a parameter isRootView, which you will set only to true when you call it from your SceneDelegate
struct BView: View {
init(isRootView: Bool = false) {
print("BView init")
self.isRootView = isRootView
}
var isRootView : Bool = false
var body: some View {
if isRootView {
NavigationView {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
else
{
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
}
And here the call from SceneDelegate
func toBView() {
let bView = BView(isRootView: true)
window?.rootViewController = UIHostingController(rootView: bView)
}

SwiftUI and AppKit: Use close dialog to ask if the app is allowed to quit

I am using Big Sur and SwiftUI with the SwiftUI lifecycle. I want to implement an alert, where the user gets asked, if the application can be quit or not. How is this possible with SwiftUI? It should look like this:
It's possible by using this code (this code opens the Alert only in the key window):
import SwiftUI
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
#Published var willTerminate = false
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
// check, if at least one window is open:
if NSApplication.shared.windows.count == 0 {
// if no one is open, close it
return .terminateNow
}
// if one or more are open, set the willTerminate variable, so that the alert can be shown
self.willTerminate = true
// return a .terminateLater (to which we need to reply later!)
return .terminateLater
}
/// This method tells the application, that it should not close
func `continue`() {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
/// This method closes the application
func close() {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
#main
struct WindowShouldCloseApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
And this is the ContentView.swift:
import SwiftUI
struct ContentView: View {
#EnvironmentObject private var appDelegate: AppDelegate
#State private var window: NSWindow?
var body: some View {
Text("Hello, world!")
.padding()
.background(WindowAccessor(window: self.$window)) // access the window
.alert(isPresented: Binding<Bool>(get: { self.appDelegate.willTerminate && self.window?.isKeyWindow ?? false }, set: { self.appDelegate.willTerminate = $0 }), content: {
// show an alert, if the application should be closed
Alert(title: Text("Really close?"),
message: Text("Do you really want to close the application?"),
primaryButton: .default(Text("Continue"), action: { self.appDelegate.continue() }),
secondaryButton: .destructive(Text("Close"), action: { self.appDelegate.close() }))
})
}
}
// thanks to Asperi: https://stackoverflow.com/questions/63432700/how-to-access-nswindow-from-main-app-using-only-swiftui/63439982#63439982
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}

How can I use lifecycle methods when a particular view is selected?

I'm currently developing an application using SwiftUI.
This app has 2 Views controlled a Tab View.
I want to use these methods sceneDidBecomeActive and sceneWillEnterForeground in SceneDelegate.swift only when a particular view is selected.
These methods work irrespective of which view is selected.
How can I do this request?
SceneDelegate.swift
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
// I want use this print method only when FirstView is selected
print("selected FirstVIew")
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
// I want use this print method only when FirstView is selected
print("selected FirstVIew")
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Text("First")
}.tag(1)
SecondView()
.tabItem {
Text("Second")
}.tag(2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("FirstView")
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("SecondView")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
Xcode: Version 11.7
Swift: Swift 5
SceneDelegate methods deal with App's life cycle, not a view's. Therefore you cannot "run" them when a view is selected.
What you can do though is use UserDefaults.
// When first view selected
UserDefaults.standard.set("First View", forKey: "selectedView")
// In SceneDelegate
func sceneDidBecomeActive(_ scene: UIScene) {
if let selected = UserDefaults.standard.string(forKey: "selectedView"),
selected == "First View" {
print("selected FirstVIew")
}
}
func sceneWillEnterForeground(_ scene: UIScene) {
if let selected = UserDefaults.standard.string(forKey: "selectedView"),
selected == "First View" {
print("selected FirstVIew")
}
}

problems with navigation in SwiftUI (1.0)

first i have NavigationView and dont working swipe back, how in UIKit NavigationController, why?
Second, how to correctly create the navigation flow for my situation:
I have AuthView(), SelectedLanguageView() and OnboardingView().
At first start user show SelectedLanguageView(), next OnboardingView() and in the end AuthView()
User dont can with OnboardingView() return on SelectedLanguageView() and with AuthView() return AuthView()
Just NavigationLink dont working without NavigationView, why?
if this is not the first launch, and user not authorized, then show AuthView(), else if user authorize, then show MainView(),
MainView() include tab bar for 4 items, every item have NavigationView
#State private var navBarHidden = true
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let languagSettings = UserLanguageSettings()
UserDefaults.standard.set(false, forKey: "isNotFirstLaunchApp")
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if UserDefaults.standard.bool(forKey: "isNotFirstLaunchApp") == false {
window.rootViewController = UIHostingController(rootView: OnboardingView(navBarHidden: $navBarHidden).environmentObject(languagSettings))
} else {
window.rootViewController = UIHostingController(rootView: AuthorizationView(navBarHidden: $navBarHidden.not, isAddNavView: true).environmentObject(languagSettings))
}
self.window = window
window.makeKeyAndVisible()
}
}
condition if the user is authorized has not added yet
var isAddNavView: Bool = false
var body: some View {
if isAddNavBar {
return NavigationView {
ZStack {
VStack {
Text("")
}
}
}
} else {
return ZStack {
VStack {
Text("")
}
}
}
}
i have error, if return NavigationView, if delete NavigationView and return ZStack, then all right. I think there is a better solution.
ideas?
NavigationLink(destination: SignInView(), isActive: $isShowingSignInView) {
EmptyView()
}
Button(action: {
self.isShowingSignInView.toggle()
}) {
Text("isShowingSignInView")
}
all navigation links in the project i use like this
next - i tried the following:
in all project i delete all NavigationView and create modal show screens (.sheet)
in AuthorizationView() i have variables #State
Example, on AuthorizationView() i have
#State var isShowRegistration: Bool = false
and tap button Registration showing flow Registration include 4 screens which are shown with .sheet
RegistrationView(registrationView: $isShowRegistration)
//screens below include #Binding var registrationView: Bool
VerificationView(registrationView: $registrationView)
SetupPasswordView(registrationView: $registrationView)
EndRegistrationViewAndAuthorization(registrationView: $registrationView)
on EndRegistrationViewAndAuthorization i have button and in ideal me need closed all previous screens and call func Auth() Which will open me the main screen of the application with the tab bar each item of which includes NavigationView. Now my button action {self.registrationView.toogle()} return me RegistrationView() but should return to AuthorizationView()
i have error, if return NavigationView, if delete NavigationView and return ZStack, then all right. I think there is a better solution. ideas?
For SwiftUI 1.0 the following should work
var body: some View {
Group {
if isAddNavBar {
NavigationView {
ZStack {
VStack {
Text("")
}
}
}
} else {
ZStack {
VStack {
Text("")
}
}
}
}
}

SwiftUI - PresentationButton with modal that is full screen

I am trying to implement a button that presents another scene with a "Slide from Botton" animation.
PresentationButton looked like a good candidate, so I gave it a try:
import SwiftUI
struct ContentView : View {
var body: some View {
NavigationView {
PresentationButton(destination: Green().frame(width: 1000.0)) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone X")
.colorScheme(.dark)
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)"
)
}
}
}
#endif
And here is the result:
I want the green view to cover the whole screen, and also the modal to be not "draggable to close".
Is it possible to add modifier to PresentationButton to make it full screen, and not draggable?
I have also tried a Navigation Button, but:
- It doesn't "slide from bottom"
- It creates a "back button" on detail view, which I don't want
thanks!
Unfortunately, as of Beta 2 Beta 3, this is not possible in pure SwiftUI. You can see that Modal has no parameters for anything like UIModalPresentationStyle.fullScreen. Likewise for PresentationButton.
I suggest filing a radar.
The nearest you can currently do is something like:
#State var showModal: Bool = false
var body: some View {
NavigationView {
Button(action: {
self.showModal = true
}) {
Text("Tap me!")
}
}
.navigationBarTitle(Text("Navigation!"))
.overlay(self.showModal ? Color.green : nil)
}
Of course, from there you can add whatever transition you like in the overlay.
Although my other answer is currently correct, people probably want to be able to do this now. We can use the Environment to pass a view controller to children. Gist here
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: UIViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
Add an extension to UIViewController
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value: toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
And whenever we need it, use it:
struct MyView: View {
#Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
[EDIT] Although it would be preferable to do something like #Environment(\.viewController) var viewController: UIViewController? this leads to a retain cycle. Therefore, you need to use the holder.
Xcode 12.0 - SwiftUI 2 - iOS 14
Now possible. Use fullScreenCover() modifier.
var body: some View {
Button("Present!") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
Hacking With Swift
This version fixes the compile error present in XCode 11.1 as well as ensures that controller is presented in the style that is passed in.
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
self.present(toPresent, animated: true, completion: nil)
}
}
To use this version, the code is unchanged from the previous version.
struct MyView: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
My solution for this (which you can easily extend to allow other params on the presented sheets to be tweaked) is to just subclass UIHostingController
//HSHostingController.swift
import Foundation
import SwiftUI
class HSHostingControllerParams {
static var nextModalPresentationStyle:UIModalPresentationStyle?
}
class HSHostingController<Content> : UIHostingController<Content> where Content : View {
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let nextStyle = HSHostingControllerParams.nextModalPresentationStyle {
viewControllerToPresent.modalPresentationStyle = nextStyle
HSHostingControllerParams.nextModalPresentationStyle = nil
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
use HSHostingController instead of UIHostingController in your scene delegate
like so:
// Use a HSHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//This is the only change from the standard boilerplate
window.rootViewController = HSHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
then just tell the HSHostingControllerParams class what presentation style you want before triggering a sheet
.navigationBarItems(trailing:
HStack {
Button("About") {
HSHostingControllerParams.nextModalPresentationStyle = .fullScreen
self.showMenuSheet.toggle()
}
}
)
Passing the params via the class singleton feels a little 'dirty', but in practice - you would have to create a pretty obscure scenario for this not to work as expected.
You could mess around with environment variables and the like (as other answers have done) - but to me, the added complication isn't worth the purity.
update: see this gist for extended solution with additional capabilities
So I was struggling with that and I didn't like the overlay feature nor the ViewController wrapped version since it gave me some memory bug and I am very new to iOS and only know SwiftUI and no UIKit.
I developed credits the following with just SwiftUI which is probably what an overlay does but for my purposes it is much more flexible:
struct FullscreenModalView<Presenting, Content>: View where Presenting: View, Content: View {
#Binding var isShowing: Bool
let parent: () -> Presenting
let content: () -> Content
#inlinable public init(isShowing: Binding<Bool>, parent: #escaping () -> Presenting, #ViewBuilder content: #escaping () -> Content) {
self._isShowing = isShowing
self.parent = parent
self.content = content
}
var body: some View {
GeometryReader { geometry in
ZStack {
self.parent().zIndex(0)
if self.$isShowing.wrappedValue {
self.content()
.background(Color.primary.colorInvert())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
}
Adding an extension to View:
extension View {
func modal<Content>(isShowing: Binding<Bool>, #ViewBuilder content: #escaping () -> Content) -> some View where Content: View {
FullscreenModalView(isShowing: isShowing, parent: { self }, content: content)
}
}
Usage:
Use a custom view and pass the showModal variable as a Binding<Bool> to dismiss the modal from the view itself.
struct ContentView : View {
#State private var showModal: Bool = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.showModal.toggle()
}
}, label: {
HStack{
Image(systemName: "eye.fill")
Text("Calibrate")
}
.frame(width: 220, height: 120)
})
}
.modal(isShowing: self.$showModal, content: {
Text("Hallo")
})
}
}
I hope this helps!
Greetings krjw