How to use an NSAlert with storyboards - swift

I'm teaching myself Swift (currently using Xcode 7.3) and I'm working with storyboards for the first time. I'm writing an OS X-based app and I want to display an alert when the user attempts to load data when data already exists. I've read the following thread, Add completion handler to presentViewControllerAsSheet but I'm having trouble wrapping my head around closures/completion handlers. I understand them "in theory" but not yet well enough to write one.
In the thread above, a Struct is being returned. I just need to return an Int or Bool to indicate whether the user wants to overwrite the data or not.

You don't need to create a second view controller. Just configure and display an NSAlert object:
#IBAction func loadData(sender : AnyObject) {
let dataAlreadyExists = true // assume this is always true
if dataAlreadyExists {
let alert = NSAlert()
alert.messageText = "Do you want to reload data?"
alert.addButtonWithTitle("Reload")
alert.addButtonWithTitle("Do not reload")
alert.beginSheetModalForWindow(self.view.window!) { response in
if response == NSAlertFirstButtonReturn {
// reload data
}
}
}
}

Related

Setting Observer for Swift Objects/Properties

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.

macOS Swift 3 - Handling notification alert action button

I'm new to Swift (but not to programming). I have simple app that provides an alert based on specific conditionals. I would like to execute a function (or even just set a variable) when one of the buttons is pressed. Ideally, I just need one button, but if for whatever reason, only the notification.actionButtonTitle can have a handler, that's fine with me.
My notification code is currently in a Swift file as a helper.
import Foundation
class NotificationHelper {
static func sampleNotification(notification: NSUserNotification) {
let notificationCenter = NSUserNotificationCenter.default
notification.identifier = "unique-id-123"
notification.hasActionButton = true
notification.otherButtonTitle = "Close"
notification.actionButtonTitle = "Show"
notification.title = "Hello"
notification.subtitle = "How are you?"
notification.informativeText = "This is a test"
notificationCenter.deliver(notification)
}
}
Currently in AppDelegate, this is defined:
let notification = NSUserNotification()
…and I call the notification like this:
NotificationHelper.sampleNotification(notification: notification)
The resulting notification works, as you can see in the screenshot below. However, I cannot seem to listen to the button action. I have tried adding this to the AppDelegate as well as the NotificationHelper file, but I did not have any success with it:
func userNotificationCenter(center: NSUserNotificationCenter, didActivateNotification notification: NSUserNotification) {
print("checking notification response")
}
Any idea of what I'm missing?
Thanks!
You'll need to assign something as the delegate of the NSUserNotificationCenter:
NSUserNotificationCenter.default.delegate = self
If you add this to your AppDelegate and make your AppDelegate conform to NSUserNotificationCenterDelegate:
class AppDelegate: NSObject, NSUserNotificationCenterDelegate {
}

Pass saved Workout from Watch to iPhone

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.

Determine if Finder can be safely killed

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

Swift UiAlertController after another at runtime

I found some solutions solving the problem to show more than one UIAlertController but they aren't working for me. While starting the app it could be possible that more than one alert needs to be shown. So when I call the showAlert() function I do not know if there will be more alerts. With rootViewController.presentViewController(..) it's only possible to show one and the others will be ignored. So what I need to do is to check if one alert is presented otherwise queue the alert and after closing the first alert check if there are more alerts to show. My idea was to solve this with NSOperation and NSOperationQueue, saying that just one Operation can be executed in time. Something like this:
var alertQueue : NSOperationQueue = NSOperationQueue()
func showAlert(alertController: UIAlertController) {
alertQueue.maxConcurrentOperationCount = 1
alertQueue.addOperation(NSOperationAlert(alertController: alertController))
}
The NSOperation:
class NSOperationAlert: NSOperation {
let aController : UIAlertController
init(alertController : UIAlertController) {
aController = alertController
}
override func start() {
AppRouter.sharedInstance.window.rootViewController?.presentViewController(aController, animated: true, completion: nil)
}
}
But obviously something is missing. I don't know exactly how to solve that the next queued alert is shown after the other alert isn't shown anymore. In other words how do I know that the currently presented Alert is "finished" without adding a handler to each button and tell this my alertQueue to execute the next alert if there is one.
Thanks a lot !