On macOS unlike iOS, it appears if you want to disable reopening documents at launch, you need to rely on the application delegate notifications vs the newer methods - with an options argument, like on iOS:
applicationWillFinishLaunching(_:), here you want to instantiate your sub-classed document controller
// We need our own to reopen our "document" urls
_ = DocumentController.init()
applicationDidFinishLaunching(_:), here you want to inspect the supplied userInfo
if let info = note.userInfo{
if let launchURL = info[NSApplication.launchIsDefaultUserInfoKey] as? String {
Swift.print("launchIsDefaultUserInfoKey: notif \(launchURL)")
disableDocumentReOpening = launchURL.boolValue
}
if let notif = info[NSApplication.launchUserNotificationUserInfoKey] as? String {
Swift.print("applicationDidFinishLaunching: notif \(notif)")
disableDocumentReOpening = true
}
}
so when my document controller is called to do the doc restores, it would see this flag within the app delegate: var disableDocumentReOpening = false.
func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: #escaping (NSWindow?, Error?) -> Void) {
if (NSApp.delegate as! AppDelegate).disableDocumentReOpening {
completionHandler(nil, NSError.init(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) )
}
else
{
NSDocumentController.restoreWindow(withIdentifier: identifier, state: state, completionHandler: completionHandler)
}
}
but unfortunately, I have something wrong but what? Manually launching the app in debugger, it appears the document controller restore call is ahead of the app delegate's routine to inspect the userInfo.
I had read a SO post on this, showing an objective-c code snippet, but flag was local to the controller, but didn't understand how you could have a read/write class var - as I tried. Also didn't understand its use of a class function as doc says its an instance method, but trying that as well didn't work either.
What am I missing?
Related
I've been looking for a way to trigger a method when the number of displays connected to a mac changes. I know I can get the value of NSScreen.screens.count, but I need to find a way to create a notification or something when that value changes or something else that would indicate a change in the number of displays connected.
I have tried KVO examples here and here, but in order for either of those to work there needs to be an event that triggers the methods inside the class.
In essence this is what I would like to do based on the first link:
class EventObserverDemo {
var statusObserver: NSKeyValueObservation?
var objectToObserve: NSScreen.screens.count?
func registerAddObserver() -> Void {
statusObserver = objectToObserve?.observe(NSScreen.screens.count, options: [.new, .old], changeHandler: {[weak self] (NSScreen.screens.count, change) in
if let value = change.newValue {
// observed changed value and do the task here on change.
print("The display count has changed.")
}
})
}
func unregisterObserver() -> Void {
if let sObserver = statusObserver {
sObserver.invalidate()
statusObserver = nil
}
}
}
I tried using a notification that used NSScreen.colorSpaceDidChangeNotification but that does not trigger a notification if a display is disconnected.
I would like to find a way to detect any time an external display is connected or disconnected. There has to be something I haven't found yet because whenever I plug in an external display to my mac I see the screen on the main display change, so there's some kind of notification that something changed whether I plug in a display or unplug it from my mac.
I looked at the didSet function, but I couldn't figure out a way to implement that on NSScreen.screens.count property.
I also looked into a property wrapper for NSScreen.screens.count but again I couldn't figure that out either.
You can observe the NSApplication.didChangeScreenParametersNotification notification. This example will only print once each time a display is either connected or disconnected, and what the change was in the number of screens.
Code:
class EventObserverDemo {
var lastCount = NSScreen.screens.count
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(trigger),
name: NSApplication.didChangeScreenParametersNotification,
object: nil
)
}
#objc private func trigger(notification: NSNotification) {
let newCount = NSScreen.screens.count
if newCount != lastCount {
print("Switched from \(lastCount) to \(newCount) displays")
lastCount = newCount
}
}
}
You don't need to remove/invalidate the observer either, easier to let the system handle it:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you do not need to unregister an observer that you created with this function. If you forget or are unable to remove an observer, the system cleans up the next time it would have posted to it.
I'm working on a macOS desktop app in Swift 4.
It has a WKWebView which loads up a web page that sends notifications.
None of the notifications are shown by default and there's also no permission request.
I need a way to show the notifications and intercept them, so that I can show a counter.
Any idea how to achieve this?
I was facing the same challenge and solved it by injecting a script (WKUserScript) which overrides the web notification API with a custom implementation that leverages the WKUserContentController to send messages to the native app code which posts the final notifications in the end.
Setting up WKWebView
Programmatic creation of a WKWebView is necessary to use a custom WKWebViewConfiguration as far as I know. Creating a new macOS app project I extend my viewDidLoad in the ViewController function like this:
override func viewDidLoad() {
super.viewDidLoad()
let userScriptURL = Bundle.main.url(forResource: "UserScript", withExtension: "js")!
let userScriptCode = try! String(contentsOf: userScriptURL)
let userScript = WKUserScript(source: userScriptCode, injectionTime: .atDocumentStart, forMainFrameOnly: false)
let configuration = WKWebViewConfiguration()
configuration.userContentController.addUserScript(userScript)
configuration.userContentController.add(self, name: "notify")
let documentURL = Bundle.main.url(forResource: "Document", withExtension: "html")!
let webView = WKWebView(frame: view.frame, configuration: configuration)
webView.loadFileURL(documentURL, allowingReadAccessTo: documentURL)
view.addSubview(webView)
}
First I load the user script from the app bundle and add it to the user content controller. I also add a message handler called notify which can be used to phone back from the JavaScript context to native code. At the end I load an example HTML document from the app bundle and present the web view using the whole area available in the window.
Overriding the Notification API
This is the injected user script and a partial override of the web Notification API. It is sufficient to handle the typical notification permission request process and posting of notifications in scope of this generic question.
/**
* Incomplete Notification API override to enable native notifications.
*/
class NotificationOverride {
// Grant permission by default to keep this example simple.
// Safari 13 does not support class fields yet, so a static getter must be used.
static get permission() {
return "granted";
}
// Safari 13 still uses callbacks instead of promises.
static requestPermission (callback) {
callback("granted");
}
// Forward the notification text to the native app through the script message handler.
constructor (messageText) {
window.webkit.messageHandlers.notify.postMessage(messageText);
}
}
// Override the global browser notification object.
window.Notification = NotificationOverride;
Every time a new notification is created in the JavaScript context, the user content controller message handler is invoked.
Handling the Script Message
The ViewController (or whatever else should handle the script messages) needs to conform to WKScriptMessageHandler and implement the following function to handle invocations:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let content = UNMutableNotificationContent()
content.title = "WKWebView Notification Example"
content.body = message.body as! String
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: nil)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in
if error != nil {
NSLog(error.debugDescription)
}
}
}
The whole implementation is about the creation of a local, native notification in macOS. It does not work yet without additional effort, though.
App Delegate Adjustments
Before a macOS app is allowed to post notifications, it must request the permissions to do so, like a website.
func applicationDidFinishLaunching(_ aNotification: Notification) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (granted, error) in
// Enable or disable features based on authorization.
}
}
If notifications should be presented while the app is in the foreground, the app delegate must be extended further to conform to UNUserNotificationCenterDelegate and implement:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(UNNotificationPresentationOptions.alert)
}
which requires the delegate assignment in applicationDidFinishLaunching(_:):
UNUserNotificationCenter.current().delegate = self
The UNNotificationPresentationOptions may vary according to your requirements.
Reference
I created an example project available on GitHub which renders the whole picture.
I’m relatively new to Realm. My task is to bundle a RealmDB and make it writable. Thus far I have copied the bundled realm file into the project and implemented the following code in the app delegate. Above the "func application(application: UIApplication, didFinishLaunchingWithOptions” I used the following function:
func bundleURL(name: String) -> NSURL? {
return NSBundle.mainBundle().URLForResource("data", withExtension: "realm") }
And below didFinishLaunchingWithOptions, I used the following:
if let v0URL = bundleURL("data.realm") {
do {
try NSFileManager.defaultManager().removeItemAtURL(defaultURL)
try NSFileManager.defaultManager().copyItemAtURL(v0URL, toURL: defaultURL)
} catch {}
The issue is that I have to load the app twice to get the data to show up in a MapViewController, which is the first controller upon launch. In this case, I want map pins in the MapViewController to automatically appear upon build. I tried to implement a notification in the MapViewController using the following:
let results = try! Realm().objects(Spaces)
notificationToken = results.addNotificationBlock {[weak self](changes: RealmCollectionChange<Results<Sapces>>) in
self!.populateMap()
I also tried to implement a Database Manager:
func getDBItems() -> [Spaces] {
let dbItemsFromRealm = try! Realm().objects(Spaces)
var bathroom = [Spaces]()
if dbItemsFromRealm.count > 0 {
for dbItemsInRealm in dbItemsFromRealm {
let spaces = dbItemsInRealm as Spaces
space.append(space)
}
}
return space
}
}
However, I can’t get the pins to load upon launch. Any help would be much appreciated.
The behavior you describe is what I'd expect to see if you've already opened the Realm at the target path prior to copying the bundled Realm over to that location. You can confirm this by putting a breakpoint on the Realm initializer and on your code that calls removeItemAtURL and seeing which is hit first.
On Watch, I'm able to pass a saved workout from the WorkoutInterfaceController to the SummaryInterfaceController. But I was wondering how to pass the saved workout from the Watch to the iPhone (so I can display it in a Summary View Controller too).
Do you know? Or is there a better way I'm supposed to do this?
Here's what I use to pass the saved workout from WorkoutInterfaceController to the SummaryInterfaceController:
private func saveWorkout() {
// Create and save a workout sample
let configuration = workoutSession!.workoutConfiguration
let isIndoor = (configuration.locationType == .indoor) as NSNumber
print("locationType: \(configuration)")
let workout = HKWorkout(activityType: configuration.activityType,
start: workoutStartDate!,
end: workoutEndDate!,
workoutEvents: workoutEvents,
totalEnergyBurned: totalEnergyBurned,
totalDistance: totalDistance,
metadata: [HKMetadataKeyIndoorWorkout:isIndoor]);
healthStore.save(workout) { success, _ in
if success {
self.addSamples(toWorkout: workout)
}
}
WKInterfaceController.reloadRootControllers(withNames: ["SummaryInterfaceController"], contexts: [workout])
}
private func addSamples(toWorkout workout: HKWorkout) {
// Create energy and distance samples
let totalEnergyBurnedSample = HKQuantitySample(type: HKQuantityType.activeEnergyBurned(),
quantity: totalEnergyBurned,
start: workoutStartDate!,
end: workoutEndDate!)
// Add samples to workout
healthStore.add([totalEnergyBurnedSample], to: workout) { (success: Bool, error: Error?) in
if success {
// Samples have been added
}
}
}
Let me know if any questions or information needed, thanks!
As a part of my research and development,I discovered how the pairing of the iPhone and the Apple Watch has the potential to be useful.
In this case, tapping on a button on the Watch app will send text on the iPhone.
To make a simple demo of this functionality, place a button on the WatchKit interface and a label on the iOS app’s storyboard.
Now, hook up the button to the WatchKit Interface Controller as an IBAction in order to respond to button tap events. Also hook up the Label to the UI View Controller as an IBOutlet.
In the Interface Controller, we make up a string variable to send to the label and in the button’s IBAction method, make a dictionary that includes the string variable you made. This dictionary is what is passed to the iPhone app.
class InterfaceController: WKInterfaceController {
Var str: String = "Hello Phone"
#IBAction func button() {
let dict: Dictionary = ["message": str]
}
Use the following method to send the dictionary to the iPhone.
WKInterfaceController.openParentApplication(dict, reply: {(reply, error) -> void in
print("Reply receive from iPhone app")
})
In the AppDelegate of the iOS app, add the following application method. This is what will handle the previous methods communication from the Watch. Also we can use a notification to notify a view controller that data has been received and to pass it along.
func application(application: UIApplication, handleWatchkitExtensionRequest userInfo: [NSObject : AnyObject]?, reply:(([NSObject : AnyObject]!) {
NSNotificationCenter.defaultCenter().postNotificationName("WatchKitReq", object: userInfo)
}
Finally in the view controller, make a listener for the notification that will update the label’s text to the string that was sent with the data.
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector("handleWatchKitNotification:"), name: "WatchKitReq", object: nil)
}
func handleWatchKitNotification(notification: NSNotification) {
if let userInfo = notification.object as? [String : String] {
label.text = userInfo["message"]
}
}
Hope this will help you to understand. For more concerns you can look on this,
Delegate Method
To do this you have to create an App Group which is essentially a space which both apps can use. It was brought in with the exetension framework in iOS8 so apps can communicate with their Today widgets, or custom keyboards, and amongst other applications.
ADD CAPABILITIES
The first thing we have to do is add the app group capability to both our iPhone and Watch Watch Extension targets.
To do this open up the project settings (blue icon at the top of the list of files) and select the iPhone target. You will need to select the “capabilities” tab at the top of the page and scroll down to turn on app groups.
This requires a connected developer profile, and will take a while to enable. You’ll need to do the same steps to switch on app groups for the watch kit extension also.
Next you need to ensure that the app group string is an identifier string you want and that makes sense for your app, it must start with the word group or it complains. You can also add multiple groups if you wish. Whatever you pick they must be enabled with a blue tick (again this might take a while) and are exactly the same for both the iPhone and Watch extension targets!
To use App Groups, it’s not that different or difficult to use than NSUserDefaults:
var userDefaults = NSUserDefaults(suiteName: "group.com.example.My-App")
userDefaults.setObject(true, forKey: "isDarkModeEnabled")
userDefaults.synchronize()
The only differences here are how NSUserDefaults is instantiated and calling synchronize at the end. You feed it the container ID to the constructor parameter called “suiteName”, then call “synchronize()”, and your data flies to the cloud for other apps and devices to consume.
Taking It to the Next Level
You can take this one step further by creating a class for your app and abstract the underlying storage for your properties. Here’s an example:
public class ConfigurationModel: NSObject {
public static let storageKey = "group.com.example.My-App"
public let userStorage = NSUserDefaults(suiteName: storageKey)
public var isDarkModeEnabled: Bool {
get {
// Get setting from storage or default
if userStorage?.objectForKey("isDarkModeEnabled") == nil {
userStorage?.setObject(false, forKey: "isDarkModeEnabled")
userStorage?.synchronize()
}
return userStorage?.objectForKey("isDarkModeEnabled")
}
set {
// Set new value in storage
userStorage?.setObject(newValue, forKey: "isDarkModeEnabled")
userStorage?.synchronize()
}
}
At the top of the class, I am declaring my group container ID and creating the NSUserDefault object out of it. Then my properties for the class have getters and setters to store the data to the App Group. If the key doesn’t exist, it creates it with a default value and synchronizes it. Using the class from this point forward is simple:
var configModel = ConfigurationModel()
configModel.isDarkModeEnabled = true
This property is stored in the cloud! Everything is abstracted away for you. You don’t have to be bothered about storing and synchronizing it into the App Group. It’s all done automatically for you in the getters and setters!
Hope, this will help you to understand how you can share data between the iPhone and Apple Watch app.
I am currently developing a utility program that requires the Finder to be restarted after some changes are made to the user's defaults.
To be on the safe side, I would like to check if the Finder is busy before calling killall Finder (via NSTask). If the Finder is copying files or otherwise busy, I would like to prevent the action and wait a little.
Is there a way to determine if the Finder is busy or if it can safely be killed, in Swift 2.3 on macOS 10.10+ ?
In case this is not possible, is there a safer way for me to refresh (restart) the Finder?
Thanks!
Thanks to #dfri 's comment, I was able to figure out a way (albeit not exactly the one presented in the linked answer) to do this.
Since observing the NSRunningApplication object for the Finder was not possible (the object was deinitialized due to termination before I could remove the observer), I ended up observing NSWorkspaceDidTerminateApplicationNotification from NSWorkspace.sharedWorkspace().notificationCenter
NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self, selector: #selector(MyController.applicationWasTerminated(_:)), name: NSWorkspaceDidTerminateApplicationNotification, object: nil)
I can then remove this observer when my controller is deinitialized, and the selector looks like this :
func applicationWasTerminated(notification: NSNotification?) {
guard let notif = notification else { return }
guard let userInfo = notif.userInfo as? [String : AnyObject] else { return }
guard let identifier = userInfo["NSApplicationBundleIdentifier"] as? String else { return }
if identifier == "com.apple.finder" {
NSWorkspace.sharedWorkspace().launchAppWithBundleIdentifier("com.apple.finder", options: NSWorkspaceLaunchOptions.Default, additionalEventParamDescriptor: nil, launchIdentifier: nil)
}
}