I recently learned how to run code whenever my app is opened by using onReceive and onAppear, as well as scenePhase. These work perfectly when the app is still in the background. However, when I terminate the app by swiping out of it after double tapping Home, and then open it, none of them work. How do I make the code run whenever the app is opened, even if it is not in the background?
Here is my code for on-open functions:
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
//open QR Scanner when app is resumed
print("Active")
return
case .background:
print("Background")
return
case .inactive:
print("Inactive")
#unknown default:
return
}
}
.onAppear {
print("opened!")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
print("opened! 2")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
print("opened! 3")
}
This code has two different options for running code that will only run when the app is launched (eg "swiped out and then opened again").
Keep in mind that you will not see the printed lines in the debugger the second time as the debugger will not be attached to the new instance -- Xcode only attaches to the first instance that you run.
#objc class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("launch")
return true
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
print("App runs")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Since the debugger console won't show the prints on the second launch, here's an example proving that the init code runs that you can see in the UI:
#main
struct MyApp: App {
var appState = AppState()
init() {
print("App runs")
appState.count += 1
}
var body: some Scene {
WindowGroup {
ContentView(appState: appState)
}
}
}
class AppState : ObservableObject {
#Published var count = 0
}
struct ContentView : View {
#ObservedObject var appState : AppState
var body: some View {
Text("Hello: \(appState.count)")
}
}
Related
I am struggling with a bug and I just can't seem to solve it, or where to look further.
The problem occurs when I try to remove a view (which holds a NavigationView) from the view hierarchy. It crashes with: Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
After experimenting with the sanitizer I got this output in the debugger: *** -[_TtGC7SwiftUI41StyleContextSplitViewNavigationControllerVS_19SidebarStyleContext_ removeChildViewController:]: message sent to deallocated instance 0x10904c880
Which pointed me to figure out that it was the NavigationView that cause it somehow. But I still can't figure out how to get from here.
This problem ONLY occurs on a real device, it works just fine in a simulator and you may have to hit the login, and then log out and log back in a few times before the crash happens.
I made a sample app with the example: https://github.com/Surferdude667/NavigationRemoveTest
The code is as follows:
NavigationRemoveTestApp
#main
struct NavigationRemoveTestApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
RootView
struct RootView: View {
#StateObject private var viewModel = RootViewModel()
var body: some View {
if !viewModel.loggedIn {
WelcomeView()
} else {
ContentView()
}
}
}
RootViewModel
class RootViewModel: ObservableObject {
#Published var loggedIn = false
init() {
LogInController.shared.loggedIn
.receive(on: DispatchQueue.main)
.assign(to: &$loggedIn)
}
}
WelcomeView
struct WelcomeView: View {
var body: some View {
NavigationView {
VStack {
Text("Welcome")
NavigationLink("Go to login") {
LogInView()
}
}
}
}
}
LogInView
struct LogInView: View {
var body: some View {
VStack {
Text("Log in view")
Button("Log in") {
LogInController.shared.logIn()
}
}
}
}
ContentView
struct ContentView: View {
var body: some View {
VStack {
Text("Content view")
Button("Log out") {
LogInController.shared.logOut()
}
}
}
}
LogInController
import Combine
class LogInController {
static let shared = LogInController()
var loggedIn: CurrentValueSubject<Bool, Never>
private init() {
self.loggedIn = CurrentValueSubject<Bool, Never>(false)
}
func logIn() {
self.loggedIn.send(true)
}
func logOut() {
self.loggedIn.send(false)
}
}
I found a few solutions.
Either you wrap the if statement in the RootView with a NavigationView instead of having the NavigationView inside the actual views, it works. This is however not very convenient since everything is now wrapped in a NavigationView.
Replacing NavigationView with the new iOS 16 NavigationStack also solves it.
Omg mate, I had the same problem, because of this post it's solved. But don't you think it is a really weird bug? Did you find out more about the root cause? Nevertheless you made my day.
I'm using SwiftUI's new app lifecycle coming in iOS 14.
However, I'm stuck at how to access my AppState (single source of truth) object in the AppDelegate.
I need the AppDelegate to run code on startup and register for notifications (didFinishLaunchingWithOptions, didRegisterForRemoteNotificationsWithDeviceToken, didReceiveRemoteNotification) etc.
I am aware of #UIApplicationDelegateAdaptor but then I can not e.g. pass an object through to the AppDelegate with a constructor. I guess the other way round (creating the AppState in the AppDelegate and then accessing it in MyApp) does not work either.
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#State var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(appState)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// access appState here...
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// ...and access appState here
}
}
class AppState: ObservableObject {
// Singe source of truth...
#Published var user: User()
}
Any help is appreciated. Maybe there is currently no way to achieve this, and I need to convert my app to use the old UIKit lifecycle?
Use shared instance for AppState
class AppState: ObservableObject {
static let shared = AppState() // << here !!
// Singe source of truth...
#Published var user = User()
}
so you can use it everywhere
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var appState = AppState.shared
// ... other code
}
and
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// ...and access appState here
AppState.shared.user = ...
}
Is there a reason why you need to run the code in app delegate?
If you are using new app lifecycle, why not trigger your code from WindowGroup.onChange()
struct MyScene: Scene {
#Environment(\.scenePhase) private var scenePhase
#StateObject private var cache = DataCache()
var body: some Scene {
WindowGroup {
MyRootView()
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
cache.empty()
}
}
}
}
Apple Documentation Link
Managing scenes in SwiftUI by Majid
I'm currently making use UITabBarController in SwiftUI. Here is the implementation:
struct MyTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
public init( _ views: [AnyView]) {
self.viewControllers = views.map { UIHostingController(rootView:$0) }
}
public var body: some View {
return TabBarController(controllers: viewControllers)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
}
}
Inside of my SwiftUI I have the following:
struct ContentView: View {
var body: some View {
MyTabView([
AnyView(Text("Moo Moo")),
AnyView(MyPage())
])
}
}
struct MyPage:View {
var body:some View {
NavigationView {
VStack {
ForEach((1...10).reversed(), id: \.self) { value -> AnyView in
print("For Each Value Called")
return AnyView(MyView(text: String(value)))
}
}
}
}
}
struct MyView:View {
let text:String
var body:some View {
Text(text).onAppear {
print("On Appear Called - Making Service Call for \(text)")
}
}
}
I have the following questions:
When running this code the On Appear Called - Making Service Call for \(text), is called twice. What would cause this? My expectation is that it is only run once. Should this be occurring?
Is this a SwiftUI bug lurking around or is this expected behaviour?
Yes, your expectation would be correct. However, it looks like a bug.
The problem appear when having content inside NavigationView. If you use .onAppear() on the NavigationView, you will see it called only once. If you use onAppear() on the VStack, it's already twice.
This has reported in this thread aswell
From my view, this behavior is wrong. Maybe report to Apple or ask why
maybe I found a solution:
add on every very first NavigationLink the modifier .isDetailLink(false)
for me it stops the double onAppear calls
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) {}
}
I have a master detail structure with a list on master and a detail page where I want to present a webpage fullscreen, so no navigation bar and no status bar. The user can navigate back by a gesture (internal app).
I'm stuggeling hiding the statusbar with
.statusBar(hidden: true)
This works on master page, but not on detail page.
Hiding the navigation bar works fine with my ViewModifier
public struct NavigationAndStatusBarHider: ViewModifier {
#State var isHidden: Bool = false
public func body(content: Content) -> some View {
content
.navigationBarTitle("")
.navigationBarHidden(isHidden)
.statusBar(hidden: isHidden)
.onAppear {self.isHidden = true}
}
}
extension View {
public func hideNavigationAndStatusBar() -> some View {
modifier(NavigationAndStatusBarHider())
}
}
Any idea?
I've been trying this for a couple of hours out of curiosity. At last, I've got it working.
The trick is to hide the status bar in the Main view, whenever the user navigates to the detail view. Here's the code tested in iPhone 11 Pro Max - 13.3 and Xcode version 11.3.1. Hope you like it ;). Happy coding.
import SwiftUI
import UIKit
import WebKit
struct ContentView: View {
var urls: [String] = ["https://www.stackoverflow.com", "https://www.yahoo.com"]
#State private var hideStatusBar = false
var body: some View {
NavigationView {
List {
ForEach(urls, id: \.self) { url in
VStack {
NavigationLink(destination: DetailView(url: url)) {
Text(url)
}
.onDisappear() {
self.hideStatusBar = true
}
.onAppear() {
self.hideStatusBar = false
}
}
}
}
.navigationBarTitle("Main")
}
.statusBar(hidden: hideStatusBar)
}
}
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var url: String = ""
var body: some View {
VStack {
Webview(url: url)
Button("Tap to go back.") {
self.presentationMode.wrappedValue.dismiss()
}
Spacer()
}
.hideNavigationAndStatusBar()
}
}
public struct NavigationAndStatusBarHider: ViewModifier {
#State var isHidden: Bool = false
public func body(content: Content) -> some View {
content
.navigationBarTitle("")
.navigationBarHidden(isHidden)
.statusBar(hidden: isHidden)
.onAppear {self.isHidden = true}
}
}
struct Webview: UIViewRepresentable {
var url: String
typealias UIViewType = WKWebView
func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
let wkWebView = WKWebView()
guard let url = URL(string: self.url) else {
return wkWebView
}
let request = URLRequest(url: url)
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<Webview>) {
}
}
extension View {
public func hideNavigationAndStatusBar() -> some View {
modifier(NavigationAndStatusBarHider())
}
}
Well, your observed behaviour is because status bar hiding does not work being called from inside NavigationView, but works outside. Tested with Xcode 11.2 and(!) Xcode 11.4beta3.
Please see below my findings.
Case1 Case2
Case1: Inside any stack container
struct TestNavigationWithStatusBar: View {
var body: some View {
VStack {
Text("Hello, World!")
.statusBar(hidden: true)
}
}
}
Case2: Inside NavigationView
struct TestNavigationWithStatusBar: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.statusBar(hidden: true)
}
}
}
The solution (fix/workaround) to use .statusBar(hidden:) outside of navigation view. Thus you should update your modifier correspondingly (or rethink design to separate it).
struct TestNavigationWithStatusBar: View {
var body: some View {
NavigationView {
Text("Hello, World!")
}
.statusBar(hidden: true)
}
}
Solution for Xcode 12.5 and IOS 14.6:
Add the following to your Info.plist:
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
Add UIApplication.shared.isStatusBarHidden = false the following to your AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UIApplication.shared.isStatusBarHidden = false // -> This
return true
}
You can then hide and bring back the status bar anywhere in the app with the UIApplication.shared.isStatusBarHidden modifier.
Example:
struct Example: View {
// I'm checking the safe area below the viewport
// to be able to detect iPhone models without a notch.
#State private var bottomSafeArea = UIApplication.shared.windows.first?.safeAreaInsets.bottom
var body: some View {
Button {
if bottomSafeArea == 0 {
UIApplication.shared.isStatusBarHidden = true // or use .toggle()
}
} label: {
Text("Click here for hide status bar!")
.font(.title2)
}
}
}
I have a NavigationView that contains a view that presents a fullScreenModal. I wanted the status bar visible for the NavigationView, but hidden for the fullScreenModal. I tried multiple approaches but couldn't get anything working (it seems lots of people are finding bugs with the status bar and NavigationView on iOS14).
I've settled on a solution which is hacky but seems to work and should do the job until the bugs are fixed.
Add the following to your Info.plist:
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
And then add the following in your view's init when necessary (changing true/false as required):
UIApplication.shared.isStatusBarHidden = false
For example:
struct ContentView: View {
init() {
UIApplication.shared.isStatusBarHidden = true
}
var body: some View {
Text("Hello, world!")
}
}
It'll give you a deprecated warning, but I'm hoping this is a temporary fix.