Swift: How to add Settings to the Apple Watch App?

One of the most asked features of the Cheat-sheets app was to add the possibility to change the font size of the cheats/notes on the users’ Apple Watch. Yesterday this feature was added to the app.

So, how to add settings to the Apple Watch? In general there are two ways to proceed. One option is to add some custom “Settings” button in your iOS app and let user to configure Watch-settings. Maybe this is also the way I’ll go in the future. But for now I decided for the second option to utilize the standard way that Apple suggests, i.e. settings bundle.

How to add Settings-Watch.bundle to your app is best describe at the Apple’s docs. I also found useful this blog posting. Following steps are required:

  1. Add Settings-Watch.bundle file to your iOS target. Yes, to your iOS app and not the WatchKit target 😉
  2. Enable the App Groups capability for your iOS app, WatchKit extension, and Watch app
  3. Important: add the ApplicationGroupContainerIdentifier key to the Root.plist file of your Settings-Watch bundle. Place the key somewhere at the top level of your property list. Set its value to the identifier you specified in the App Groups capability
  4. Define your settings
  5. To localize your settings bundle just duplicate lproj folders and call them like de.lproj or ru.lproj. Use Root.strings files for translations.

So now the settings must be visible in the Watch-App of your iPhone like this:

To access the settings in your Watch app, i.e. some WKInterfaceController just use the code:

let defaults = NSUserDefaults(suiteName: "group.com.example.MyWatchKitApp")
let enabled = defaults?.boolForKey("enabled_preference")

In the Cheat-sheets app the changes of the Settings are immediately seen in the Watch-App. Be aware that your Watch app will not be notified about changes in your NSUserDefaults, because they are changes in another process. That is why this will not work:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "loadFontSize", name: NSUserDefaultsDidChangeNotification, object: nil)

You have to add KVO observer to every parameter in your NSUserDefaults:

let preferencesUserDefaults = UserDefaults(suiteName: "group.com.example.MyWatchKitApp")
                preferencesUserDefaults?.addObserver(self, forKeyPath: "enabled_preference", options: NSKeyValueObservingOptions.new, context: nil)

To listen to the changes just override the function:

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        
     // do your stuff here
    }

And of course do not forget to unregister the observer if you do not need it anymore.

Cheat-sheets version 1.21

EN

Photos! Actually, Cheat-sheets was initially planned to be a pure text based app. But this feature was so frequently asked, that it cannot be ignored anymore 😉

DE

Fotos! Eigentlich wurde Spicker anfangs als reine Text-Anwendung für kurze Notizen. Doch die Einbindung von Fotos wurde so oft gefragt, dass es nicht mehr ignoriert werden konnte 😉

RU

Фотки! Вообще то, изначально Шпаргалки задумывались как приложение исключительно для быстрых, коротких текстовых заметок. Использование фотографий осознанно не подразумевалось. Однако пользователи так часто спрашивали, что игнорировать общее настроение стало уже просто неприлично 😉

PolterApp Version 2.9

Die fortlaufende Polternummerierung ist eine angenehme Funktion. Doch manchmal ist es sinnvoll den Zähler zurückzusetzen. Mit dem neuen Update der PolterApp ist es möglich den Zähler auf beliebige Zahl zu setzen.

Die nächste Verbesserung betrifft den .csv Export. Durch die Angabe des benutzen Trennzeichens läßt sich .csv Datei mit einem Klick ins Excel importieren.

PolterApp Version 2.7-2.8

Die Benennung der Export-Dateien mit einem Statischen Namen hat sich als unpraktisch erwiesen. Die Dateien werden nun nach der Polternummer benannt. Außerdem wurden weitere Optimierungen an der App in Sachen Performance und Stabilität vorgenommen

Cheat-sheets version 1.20

EN

Current version of Cheat-sheets contains a new user preference for changing the font size of the note area. This feature was several times asked and here it is! 😉

DE

Aktuelle Version der Spicker-App bekommt neue User-Einstellung zum Setzen der Schriftgröße im Note-Bereich. Diese Feature war mehrmals nachgefragt und da ist die! 😉

RU

В Шпаргалках появилась возможность менять размер шрифта в окне для ввода записи. Этой функций интересовались многие – и вот она … нарядная на праздник к нам пришла!

Cheat-sheets versions 1.18-1.19

EN

Minor bug fixes and … TEXT FILE IMPORT! The feature that “would make many people happy” as one guy wrote once in the app review. Happy cheating 😉

DE

Kleine Fehlerbehebungen und … IMPORT VON TEXTDATEIEN! Ein Feature was “viele Leute glücklich machen wird”, so lautet ein Kommentar zur Spicker-App. Frohes Spicken 😉

RU

Исправление незначительных ошибок и … ИМПОРТИРОВАНИЕ ТЕКСТОВЫХ ФАЙЛОВ, функция которая “сделает  многих людей счастливыми”, примерно так звучал один отзыв о Шпаргалках. Удачи на экзаменах! 😉

PolterApp Version 2.6

Mit dem aktuellen Update der PolterApp bekommen die Nutzer wieder die Möglichkeit Einträge aus der Sektionstabelle zu löschen, was unter iOS 12 plötzlich nicht mehr ging. Die PolterApp wurde auch im Kern etwas überarbeitet, bzw. optimiert, so dass die App nun weniger Speicher bei der Installation benötigt.

Objective-C: how to write to the iOS photo album and get the name of the written file

Using UIImagePickerController it is easy to take a picture in your iOS app. The picture is not immediately saved in the photo library. You must apply UIImageWriteToSavedPhotosAlbum method to write it there. In some situations you need to know the name of the file that the photo library assigns to the image. For example you have an option in your app also to select an image from the photo library. And you want to avoid that the user picks the same photo. So, how to know the name of the file that photo library assigns to the photo? There is a delegate method imagePickerController:didFinishPickingMediaWithInfo:. We make a use of it:

- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
    PHAsset *asset = nil;
    PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
    fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
    PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOptions];
    if (fetchResult != nil && fetchResult.count > 0) {
        // we sorted the photos by creation date and get the last photo from Photos
        asset = [fetchResult lastObject];
    }
    
    if (asset) {
        // get photo info from this asset
        PHImageRequestOptions * imageRequestOptions = [[PHImageRequestOptions alloc] init];
        imageRequestOptions.synchronous = YES;
        [[PHImageManager defaultManager]
         requestImageDataForAsset:asset
         options:imageRequestOptions
         resultHandler:^(NSData *imageData, NSString *dataUTI,
                         UIImageOrientation orientation,
                         NSDictionary *info)
         {
             
             if ([info objectForKey:@"PHImageFileURLKey"]) {
                 // path looks like this -
                 // file:///var/mobile/Media/DCIM/###APPLE/IMG_####.JPG
                 NSURL *path = [info objectForKey:@"PHImageFileURLKey"];
                 if(path) {
                     NSString *filePath = [[LPPathUtilities applicationDocumentsFolderPath] stringByAppendingPathComponent:[path lastPathComponent]];
                     
                     NSData *dataFromImage = [NSData dataWithData:UIImageJPEGRepresentation(image, 10.0)];
                     if(![dataFromImage writeToFile:filePath atomically:YES]) {
                         // TODO: handle error
                     }
                 }
             }
         }];
    }
}

Unfortunately, the key PHImageFileURLKey is not specified in the Apple documentation. So, there is of course a danger, that this can key can change in the future. But I could not find so far a more easy approach how to get the file name. If you have a better idea, let me know 😉

WKWebView: no XSLT support

In our large iOS project that was started 2008 and that will be 10 years old this year we use much XSL transformations. This technology was the first choice at that time to deal with structured data and its representation on mobile devices. Nowadays similar uses cases can be covered by JSON and Java Script. Probably this is the reason, why WKWebView the newer component that Apple recommends to use instead of UIWebView contains no support for XSLT.

The only way to support XSLT in your app is to integrate some third party XSLT library. We decided for libxslt. That’s a C library based on libxml2. Instead of loading XML file in the WKWebView (this was possible with UIWebView) we start XSL transformation first and generate html file. Generated file is then loaded with WKWebView.

Objective C: Push Notifications in iOS 10

Apple introduced a new framework UserNotifications for delivering and handling of local and remote notifications. Let’s have a look at how to support PUSH notifications in an iOS app.

1. Import UserNotifications.framework in your AppDelegate file

#import <UserNotifications/UserNotifications.h>

Add UNUserNotificationCenterDelegate to the declaration:

#import <UserNotifications/UserNotifications.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate>
 
@end

2. Register for PUSH notifications

The best place to register for PUSH notifications is the method application:didFinishLaunchingWithOptions:. According Apple’s docs deviceToken can change from time to time. That is why it is important not to cache the device token, but to request it every time on the app start.

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    [self registerForRemoteNotifications];
    return YES;
}

- (void)registerForRemoteNotifications
{
        // iOS 10 and greater
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){
            if(!error){
                dispatch_async(dispatch_get_main_queue(), ^{
                    [[UIApplication sharedApplication] registerForRemoteNotifications];
                });
            }
        }];
}

In the code above you can see the call of requestAuthorizationWithOptions:completionHandler: method. It is important, otherwise no notifications will be displayed to the user. Quote from the Apple’s doc:

If you want your app’s remote notifications to display alerts, play sounds, or perform other user-facing actions, you must request authorization to do so using the requestAuthorizationWithOptions:completionHandler: method of UNUserNotificationCenter. If you do not request and receive authorization for your app’s interactions, the system delivers all remote notifications to your app silently.

It is also important to call registerForRemoteNotifications method on the main thread!

3. Handling of registration for remote notifications

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    // getting device token
    self.devToken = [self stringWithDeviceToken:deviceToken];
}


- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
   // TODO: handle errors here
    
}

// converts deviceToke to String
- (NSString *)stringWithDeviceToken:(NSData *)deviceToken {
    const char *data = [deviceToken bytes];
    NSMutableString *token = [NSMutableString string];
    
    for (NSUInteger i = 0; i < [deviceToken length]; i++) {
        [token appendFormat:@"%02.2hhX", data[i]];
    }
    
    return [token copy];
}

4. Handling delegate methods for UserNotifications

There are two more delegate methods to be implemented:

//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
    NSLog(@"User Info : %@",notification.request.content.userInfo);
    completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge);
}
 
//Called to let your app know which action was selected by the user for a given notification.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
    NSLog(@"User Info : %@",response.notification.request.content.userInfo);
    completionHandler();
}

In iOS 10 it is different with remote notifications during the app is in the foreground. In former iOS versions we had to handle notifications ourselves and display notification to the user, e.g. as Alert message. Now notifications are always shown.

5. Add Push Notifications Entitlements if not done yet

That’s all.
Now your app can receive remote PUSH notifications. Happy Coding!