How to extend WKExtendedRuntimeSession - swift

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.

Related

How can I disable HideTabBar and ShowAllTabs in macOS in SwiftUI? [duplicate]

I have created a very simple app on MacOS created with SwiftUI. Default there is a menu item show tab bar. How do I remove this? I doesn't make sense to have tabs in this app.
I've found the following answering the same question, but for older versions of Swift, not for SwiftUI: How do I disable the Show Tab Bar menu option in Sierra apps?
The equivalent in SwiftUI is the same thing as the equivalent in Swift (which is missed in that post for some reason). To completely remove these items from any windows for your application, in your app delegate you can set the boolean value allowsAutomaticWindowTabbing to false
func applicationWillFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
}
Thanks to #JuJoDi
In case you are using SwiftUI life cycle (Scene), you could do the following:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
}
}
I was looking for an answer for this as well and found out the following:
by default - as you already mentioned - the Show/Hide Tab is active:
There is a property on NSWindow called tabbingMode which allows us to take control by setting it to .disallowed. My problem though was: in a SwiftUI 2-lifecycle app, how can I get hold of the windows of the app?
Well, there's NSApplication.shared.windows, so my first (non working!!) attempt was to modify all the windows in my #main-App struct (as I already prevented new windows from being created, that should be suffice):
import SwiftUI
#main
struct DQ_SyslogApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
Unfortunately this did not work as NSApplication.shared.windows is empty in .onAppear.
My next step involved introducing an AppDelegate to my SwiftUI 2-lifecycle that implements applicationDidFinishLaunching(_:)...
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
print("Info from `applicationDidFinishLaunching(_:): Finished launching…")
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
...and introducing this AppDelegate to the app:
import SwiftUI
#main
struct DQ_SyslogApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
This did the trick for me:
While I would prefer to have the menu entries removed entirely, this at least prevents the user from having tabs which is what the OP was asking for.
If anybody should happen to know a solution to hide those entries, please let us know. I couldn't find a CommandGroupPlacement that represents these menus...
I found how to remove the 'Edit' menu item after reading this article
https://steipete.com/posts/top-level-menu-visibility-in-swiftui/
func applicationDidFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
if let mainMenu = NSApp .mainMenu {
DispatchQueue.main.async {
if let edit = mainMenu.items.first(where: { $0.title == "Edit"}) {
mainMenu.removeItem(edit);
}
}
}
}
2021, swiftUI 2 and higher
Result in release build:
look at places with comments "HERE":
#main
struct MyFirstApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// HERE
disallowTabbingMode()
}
#SceneBuilder
var body: some Scene {
WindowGroup {
AppMainView()
}
.commands {
//MenuLine_MyFirstApp_CustomAbout()
// HERE
MenuLine_File_NewWindow_Disable()
//MenuLine_Help_SupportEmail()
}
}
}
And method + bonuses for understanding menus in SwiftUI
// Set Custom About window instead of default
fileprivate func MenuLine_MyFirstApp_CustomAbout() -> CommandGroup<Button<Text>> {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("About") { appDelegate.showAboutWnd() }
}
}
//Add some menu button into Help menu
fileprivate func MenuLine_Help_SupportEmail() -> CommandGroup<Button<Text>> {
CommandGroup(after: CommandGroupPlacement.help) {
Button("Support Email") { NSWorkspace.shared.open("mailto:support#myfirstapp.com") }
}
}
// Disable "File -> New window" menu item (make it absent in release build)
fileprivate func MenuLine_File_NewWindow_Disable() -> CommandGroup<EmptyView> {
CommandGroup(replacing: .newItem) { }
}
fileprivate func disallowTabbingMode() {
NSWindow.allowsAutomaticWindowTabbing = false
}

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

Setup UserDefaults property as Published property in View Model [duplicate]

I have an #ObservedObject in my View:
struct HomeView: View {
#ObservedObject var station = Station()
var body: some View {
Text(self.station.status)
}
which updates text based on a String from Station.status:
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.string(forKey: "status") ?? "OFFLINE" {
didSet {
UserDefaults.standard.set(status, forKey: "status")
}
}
However, I need to change the value of status in my AppDelegate, because that is where I receive my Firebase Cloud Messages:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// Print full message.
let rawType = userInfo["type"]
// CHANGE VALUE OF status HERE
}
But if I change the status UserDefaults value in AppDelegate - it won't update in my view.
How can my #ObservedObjectin my view be notified when status changes?
EDIT: Forgot to mention that the 2.0 beta version of SwiftUI is used in the said example.
Here is possible solution
import Combine
// define key for observing
extension UserDefaults {
#objc dynamic var status: String {
get { string(forKey: "status") ?? "OFFLINE" }
set { setValue(newValue, forKey: "status") }
}
}
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.status {
didSet {
UserDefaults.standard.status = status
}
}
private var cancelable: AnyCancellable?
init() {
cancelable = UserDefaults.standard.publisher(for: \.status)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.status { // avoid cycling !!
self.status = newValue
}
})
}
}
Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage, so if you need that status only in view, you can just use
struct SomeView: View {
#AppStorage("status") var status: String = "OFFLINE"
...
I would suggest you to use environment object instead or a combination of both of them if required. Environment objects are basically a global state objects. Thus if you change a published property of your environment object it will reflect your view. To set it up you need to pass the object to your initial view through SceneDelegate and you can work with the state in your whole view hierarchy. This is also the way to pass data across very distant sibling views (or if you have more complex scenario).
Simple Example
In your SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView().environmentObject(GlobalState())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
The global state should conform ObservableObject. You should put your global variables in there as #Published.
class GlobalState: ObservableObject {
#Published var isLoggedIn: Bool
init(isLoggedIn : Bool) {
self.isLoggedIn = isLoggedIn
}
}
Example of how you publish a variable, not relevant to the already shown example in SceneDelegate
This is then how you can work with your global state inside your view. You need to inject it with the #EnvironmentObject wrapper like this:
struct ContentView: View {
#EnvironmentObject var globalState: GlobalState
var body: some View {
Text("Hello World")
}
}
Now in your case you want to also work with the state in AppDelegate. In order to do this I would suggest you safe the global state variable in your AppDelegate and access it from there in your SceneDelegate before passing to the initial view. To achieve this you should add the following in your AppDelegate:
var globalState : GlobalState!
static func shared() -> AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
Now you can go back to your SceneDelegate and do the following instead of initialising GlobalState directly:
let contentView = ContentView().environmentObject(AppDelegate.shared().globalState)

Using UIApplicationDelegateAdaptor to get callbacks from userDidAcceptCloudKitShareWith not working

I'm trying to get notified when userDidAcceptCloudKitShareWith gets called. Traditionally this was called in the App Delegate but since I am building an iOS 14+ using App as my root object. I couldn't find any documentation out yet as far as how to add userDidAcceptCloudKitShareWith to my App class, so I am using UIApplicationDelegateAdaptor to use an App Delegate class, however it doesn't seem like userDidAcceptCloudKitShareWith is ever getting called?
import SwiftUI
import CloudKit
// Our observable object class
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct SocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
let container = CKContainer(identifier: "iCloud.com.TestApp")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("did finish launching called")
return true
}
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
} }
func acceptShare(metadata: CKShare.Metadata,
completion: #escaping (Result<CKRecord.ID, Error>) -> Void) {
// Create a reference to the share's container so the operation
// executes in the correct context.
let container = CKContainer(identifier: metadata.containerIdentifier)
// Create the operation using the metadata the caller provides.
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
var rootRecordID: CKRecord.ID!
// If CloudKit accepts the share, cache the root record's ID.
// The completion closure handles any errors.
operation.perShareCompletionBlock = { metadata, share, error in
if let _ = share, error == nil {
rootRecordID = metadata.rootRecordID
}
}
// If the operation fails, return the error to the caller.
// Otherwise, return the record ID of the share's root record.
operation.acceptSharesCompletionBlock = { error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(rootRecordID))
}
}
// Set an appropriate QoS and add the operation to the
// container's queue to execute it.
operation.qualityOfService = .utility
container.add(operation)
}
}
Updated based on Asperi's Answer:
import SwiftUI
import CloudKit
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window.windowScene.delegate
window.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate: NSObject, UIWindowSceneDelegate {
let container = CKContainer(identifier: "iCloud.com...")
var originalDelegate: UIWindowSceneDelegate?
var window: UIWindow?
func sceneWillEnterForeground(_ scene: UIScene) {
print("scene is active")
}
func sceneWillResignActive(_ scene: UIScene) {
print("scene will resign active")
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene!(scene, willConnectTo: session, options: connectionOptions)
}
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
}
}
}
extension View {
func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
In Scene-based application the userDidAcceptCloudKitShareWith callback is posted to Scene delegate, but in SwiftUI 2.0 App-based application the scene delegate is used by SwiftUI itself to provide scenePhase events, but does not provide native way to handle topic callback.
The possible approach to solve this is to find a window and inject own scene delegate wrapper, which will handle userDidAcceptCloudKitShareWith and forward others to original SwiftUI delegate (to keep standard SwiftUI events working).
Here is a couple of demo snapshots based on https://stackoverflow.com/a/63276688/12299030 window access (however you can use any other preferable way to get window)
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window?.windowScene.delegate
window?.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate : NSObject, UIWindowSceneDelegate {
var originalDelegate: UISceneDelegate?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
// your code here
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene(scene, willConnectTo: session, options: connectionOptions)
}
}
Check out this question that has a lot of useful things to check across several possible answers:
CloudKit CKShare userDidAcceptCloudKitShareWith Never Fires on Mac App
Be sure to add the CKSharingSupported key to your info.plist, and then try putting the userDidAcceptCloudKitShareWith in different places using the answers in the above link (where you put it will depend on what kind of app you're building).

How do I disable the Show Tab Bar menu option in SwiftUI

I have created a very simple app on MacOS created with SwiftUI. Default there is a menu item show tab bar. How do I remove this? I doesn't make sense to have tabs in this app.
I've found the following answering the same question, but for older versions of Swift, not for SwiftUI: How do I disable the Show Tab Bar menu option in Sierra apps?
The equivalent in SwiftUI is the same thing as the equivalent in Swift (which is missed in that post for some reason). To completely remove these items from any windows for your application, in your app delegate you can set the boolean value allowsAutomaticWindowTabbing to false
func applicationWillFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
}
Thanks to #JuJoDi
In case you are using SwiftUI life cycle (Scene), you could do the following:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
}
}
I was looking for an answer for this as well and found out the following:
by default - as you already mentioned - the Show/Hide Tab is active:
There is a property on NSWindow called tabbingMode which allows us to take control by setting it to .disallowed. My problem though was: in a SwiftUI 2-lifecycle app, how can I get hold of the windows of the app?
Well, there's NSApplication.shared.windows, so my first (non working!!) attempt was to modify all the windows in my #main-App struct (as I already prevented new windows from being created, that should be suffice):
import SwiftUI
#main
struct DQ_SyslogApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
Unfortunately this did not work as NSApplication.shared.windows is empty in .onAppear.
My next step involved introducing an AppDelegate to my SwiftUI 2-lifecycle that implements applicationDidFinishLaunching(_:)...
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
print("Info from `applicationDidFinishLaunching(_:): Finished launching…")
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
...and introducing this AppDelegate to the app:
import SwiftUI
#main
struct DQ_SyslogApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
This did the trick for me:
While I would prefer to have the menu entries removed entirely, this at least prevents the user from having tabs which is what the OP was asking for.
If anybody should happen to know a solution to hide those entries, please let us know. I couldn't find a CommandGroupPlacement that represents these menus...
I found how to remove the 'Edit' menu item after reading this article
https://steipete.com/posts/top-level-menu-visibility-in-swiftui/
func applicationDidFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
if let mainMenu = NSApp .mainMenu {
DispatchQueue.main.async {
if let edit = mainMenu.items.first(where: { $0.title == "Edit"}) {
mainMenu.removeItem(edit);
}
}
}
}
2021, swiftUI 2 and higher
Result in release build:
look at places with comments "HERE":
#main
struct MyFirstApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// HERE
disallowTabbingMode()
}
#SceneBuilder
var body: some Scene {
WindowGroup {
AppMainView()
}
.commands {
//MenuLine_MyFirstApp_CustomAbout()
// HERE
MenuLine_File_NewWindow_Disable()
//MenuLine_Help_SupportEmail()
}
}
}
And method + bonuses for understanding menus in SwiftUI
// Set Custom About window instead of default
fileprivate func MenuLine_MyFirstApp_CustomAbout() -> CommandGroup<Button<Text>> {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("About") { appDelegate.showAboutWnd() }
}
}
//Add some menu button into Help menu
fileprivate func MenuLine_Help_SupportEmail() -> CommandGroup<Button<Text>> {
CommandGroup(after: CommandGroupPlacement.help) {
Button("Support Email") { NSWorkspace.shared.open("mailto:support#myfirstapp.com") }
}
}
// Disable "File -> New window" menu item (make it absent in release build)
fileprivate func MenuLine_File_NewWindow_Disable() -> CommandGroup<EmptyView> {
CommandGroup(replacing: .newItem) { }
}
fileprivate func disallowTabbingMode() {
NSWindow.allowsAutomaticWindowTabbing = false
}