Functions of WKExtensionDelegate are not called for watchOS - swift

This is my WatchDelegate.swift:
import WatchKit
class WatchDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
print("FINISHED LAUNCHING>>>>>>>>>>>") //not called
}
}
My info.plist:
My app is launching on my watch, but above delegate method is not called. Why?

In my case I didn't add anything to info.plist, I just added some magic swift sprinkles:
import SwiftUI
#main
struct WatchApp: App {
// 👇👇👇
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate: ExtensionDelegate
// 👆👆👆
var body: some Scene {
WindowGroup {
// etc
I then see logging from
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// stuff logged here shows in console
}
}

Related

How to extend WKExtendedRuntimeSession

Thank you for taking a look at this page. I am developing a Watch App and would like to do some background data processing in SwiftUI. However, the background stops after an hour at the most. how can I make it run for more than an hour?
#main
struct TestWatchOSOnly_Watch_AppApp: App {
#State var session = WKExtendedRuntimeSession()
//define and create the delegate
#State var delegate = WKDelegate()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
//create a new session
session = WKExtendedRuntimeSession()
//assign the delegate
session.delegate = delegate
//start the session
session.start()
}
}
}
}
// define the delegate and its methods
class WKDelegate: NSObject, WKExtendedRuntimeSessionDelegate{
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(reason.rawValue)
}
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("did start")
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("will expire")
}
}
It is a private application for work and will not be made public. Also, I know that there is no mention of this in the official Apple documentation. Please let me know what possible ways to go about this.

How to run watch app in the background using WKExtendedRuntimeSession

Thank you for taking a look at this page.
I am developing a Watch App and would like to do some background data processing in SwiftUI.
I tried to run it using WKExtendedRuntimeSession, but it doesn't seem to be running when it becomes inactive.
import SwiftUI
#main
struct FirebaseExample_Watch_AppApp: App {
var session = WKExtendedRuntimeSession()
init() {
session.start()
// MARK:- Extended Runtime Session Delegate Methods
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print(1)
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print(2)
}
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(3)
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
But the result is nothing happens the moment I put my arm down and the app screen disappears.
I alse tried to do this;
                 
init() {
        func startTimerButtonPressed() {
print(WKApplicationState.RawValue())
var session = WKExtendedRuntimeSession()
session.start()
print(WKExtendedRuntimeSessionState.RawValue())
}
The results are all zeros and do not appear to be initiated.
If you are familiar with WKExtendedRuntimeSession, please let me know how to run it.
Reference
https://developer.apple.com/documentation/watchkit/wkextendedruntimesession
You are not setting a delegate for your session. A possible implementation would look like this:
#main
struct TestWatchOSOnly_Watch_AppApp: App {
#State var session = WKExtendedRuntimeSession()
//define and create the delegate
#State var delegate = WKDelegate()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
//create a new session
session = WKExtendedRuntimeSession()
//assign the delegate
session.delegate = delegate
//start the session
session.start()
}
}
}
}
// define the delegate and its methods
class WKDelegate: NSObject, WKExtendedRuntimeSessionDelegate{
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(reason.rawValue)
}
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("did start")
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("will expire")
}
}

Hiding Edit Menu of a SwiftUI / MacOS app

My MacOS app doesn't have any text editing possibilities. How can I hide the Edit menu which is added to my app automatically? I'd prefer to do this in SwiftUI.
I would expect the code below should work, but it doesn't.
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
CommandGroup(replacing: .textEditing) {}
}
}
}
To my knowledge you cannot hide the whole menu, you can just hide element groups inside of it:
.commands {
CommandGroup(replacing: .pasteboard) { }
CommandGroup(replacing: .undoRedo) { }
}
For native (Cocoa) apps
It is possible to remove application menus using an NSApplicationDelegate. This approach may break in future macOS versions (e.g. if the position of the Edit menu were to change), but does currently work:
class MyAppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
let indexOfEditMenu = 2
func applicationDidFinishLaunching(_ : Notification) {
NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu)
}
}
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
For Catalyst (UIKit) apps
For Catalyst-based macOS apps, the approach is similar to that above, except that a UIApplicationDelegate deriving from UIResponder is used:
class MyAppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
override func buildMenu(with builder: UIMenuBuilder) {
/// Only operate on the main menu bar.
if builder.system == .main {
builder.remove(menu: .edit)
}
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
The current suggestions failed for me when SwiftUI updated the body of a window.
Solution:
Use KVO and watch the NSApp for changes on \.mainMenu. You can remove whatever you want after SwiftUI has its turn.
#objc
class AppDelegate: NSObject, NSApplicationDelegate {
var token: NSKeyValueObservation?
func applicationDidFinishLaunching(_ notification: Notification) {
// Remove a single menu
if let m = NSApp.mainMenu?.item(withTitle: "Edit") {
NSApp.mainMenu?.removeItem(m)
}
// Remove Multiple Menus
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Must remove after every time SwiftUI re adds
token = NSApp.observe(\.mainMenu, options: .new) { (app, change) in
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Remove a single menu
guard let menu = app.mainMenu?.item(withTitle: "Edit") else { return }
app.mainMenu?.removeItem(menu)
}
}
}
struct MarblesApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some View {
//...
}
}
Thoughts:
SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu just gets clobbered when SwiftUI wants even if you specify no commands.
This seems like a very fragile solution. There should be a way to tell SwiftUI to not touch the NSApp.mainMenu or enable more customization. Or it seems SwiftUI should check that it owned the previous menu (the menu items are SwiftUI.AppKitMainMenuItem). Or I'm missing some tool they've provided. Hopefully this is fixed in the WWDC beta?
(In Xcode 13.4.1 with Swift 5 targeting macOS 12.3 without Catalyst.)
For those of you that are looking for any updates on this - have a look at this question that I asked (and answered myself):
SwiftUI Update the mainMenu [SOLVED] kludgey
The way I got around it was to put it in a DispatchQueue.main.async closure in the AppDelegate applicationWillUpdate function
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let editMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "Edit")
if nil != editMenu {
NSApp.mainMenu?.removeItem(editMenu!)
}
}
}
}
It took me a good 4 days of searching and attempting things :) - typical that it came down to a 2 line code change

Push SwifuUIscreen from AppDelegate

How can i push a SwiftUI screen from AppDelegate, e.x. when i click on a Remote notification in willPresent Method, i want the user to redirect to my TabBarView() with tab notification selected. I also need to have initialized my rootViewModel since i have logic there.
MainApp:
#main
struct MainApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var rootViewModel: MainViewModel
init() {
self.rootViewModel = MainViewModel()
}
var body: some Scene {
WindowGroup {
if authenticated {
Tabbar()
.environmentObject(rootViewModel)
} else {
SecondScreen()
.environmentObject(rootViewModel)
}
}
}
}
AppDelegate:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresent notification")
let info = notification.request.content.userInfo
completionHandler([.badge, .sound, .banner])
}
If you want to control the tab selection programmatically from AppDelegate:
You have to use the TabBar initializer that takes a Binding to the selected tab, and
You have to be able to set the binding's value from AppDelegate.
Here's one solution.
First, define an enum representing the tab selection:
enum TabSelection: Hashable {
case firstTab
case secondTab
case thirdTab
}
Then, make your AppDelegate conform to ObservableObject and give it a Published property that holds the tab selection:
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
#Published var tabSelection: TabSelection = .firstTab
}
Finally, update your view to use tabSelection to control the selected tab:
Get a Binding<TabSelection> from appDelegate,
pass it to the TabView initializer, and
use the tag modifier on each tab subview.
struct MainApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var rootViewModel: MainViewModel
init() {
self.rootViewModel = MainViewModel()
}
var body: some Scene {
WindowGroup {
if rootViewModel.authenticated {
TabView(selection: _appDelegate.projectedValue.tabSelection) {
FirstTab()
.tag(TabSelection.firstTab)
SecondTab()
.tag(TabSelection.secondTab)
ThirdTab()
.tag(TabSelection.thirdTab)
}
.environmentObject(rootViewModel)
} else {
SecondScreen()
.environmentObject(rootViewModel)
}
}
}
}
You should be able to say $appDelegate.tabSelection to get the Binding. But I'm writing this answer on my iPad and the Swift Playgrounds app is complaining about $appDelegate, so I'm using _appDelegate.projectedValue.tabSelection to get the Binding.

Implement WatchConnectivity in an iOS App that supports iOS 8.0

My current iOS App supports iOS 8 and greater. I decided to implement a Watch App and I want to have watch and iOS app comunicate via WatchConnectivity. Let's say that I want to build a class to handle the connectivity and this class is a WCSessionDelegate. To be able to include the class in my project I've to wrap it into the #available(iOS 9.0,0) tag and that's ok...but, how can I keep track of an instance of this class in the AppDelegate (or in any other class)???
I cannot write something like
class AppDelegate: UIResponder, UIApplicationDelegate {
#available(iOS 9.0,0)
var connectivityHandler: ConnectivityHandler?
How can I use these class in a project that should support also iOS 8?!
Edit----
I've found this solution but it doesn't sound really "good". I create a dynamic property and dependently on the current system I return a instance or just nil... the property should obviously be of type AnyObject to be a valid iOS8 parameter...
var _connectivityHandler: AnyObject?
var connectivityHandler: AnyObject? {
get {
if _connectivityHandler != nil {
return _connectivityHandler
}else{
if #available(iOS 9.0, *) {
_connectivityHandler = ConnectivityHandler()
} else {
_connectivityHandler = nil
}
return _connectivityHandler
}
}
set(value) {
_connectivityHandler = value
}
}
You can move all the WatchConnectivity functions in your connectivityHandler to an extension of that class and mask that with the #availability API:
import UIKit
import WatchConnectivity
class ConnectivityHandler: NSObject {
}
#available(iOS 9.0, *)
extension ConnectivityHandler: WCSessionDelegate {
func initSession() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
}
If you do that you can add this class to your AppDelegate and it also compiles in iOS 8:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let connectivityHandler = ConnectivityHandler()
....
}