iOS: WKWebView and Clipboard API

How to copy some text into Clipboard with JavaScript within WKWebView? Sounds easy, there are however some gotchas one should know about:

  • The “new” navigator.clipboard API call is asynchronous, i.e. you get a Promise object as a result:
function copyToClipboard() {
    navigator.clipboard.writeText("Some text to copy").then(function() {
      alert('success!');
    }, function(error) {
        alert('failed with error: ' + error);
    });
    
}
  • Important note from documentation : The implementation is available through the navigator.clipboard API which must be called within user gesture event handlers like pointerdown or pointerup, and only works for content served in a secure context (e.g. https://)

This means in particular, you cannot just call navigator.clipboard.writeText where ever you want, but rather within an onClick handler, e.g.

<button name="some name" onclick="copyToClipboard();">Click me</button>

The navigator.clipboard API is supported starting with Safari 13.1

Getting started with PHP development and Laravel on MAC

Last time I got in touch with PHP development was 2004. There seems to change a lot of things in the past 17 years. Today I’m going to set up PHP and Laravel on my Mac. I know nothing about it and start with the very beginning…

So, why Laravel? I have to implement a small backend for my formapps. There are few requirements that should be met:

  • REST interface to access backend from my apps
  • user management and user roles
  • PHP language, because I have a cheap PHP hosting

I’m aware of that it is very much work, if I start from scratch. In my case I’ll have to rely on some CMS system or PHP framework. After a short research I first looked at drupal. It’s a very popular open source CMS. I had a little bit experience with it in year 2004. Drupal has a lot of features but is too much of goodness for my requirements 🙂 I need something that is more lightweight. Then I talked to a friend of mine who is backend developer. He works with Laravel. He says, it’s a very cool modern framework to get quickly started with backend development. Well, why not give it a try?

Setting up environment

According to Laravel documentation, there are several ways how to start working with Laravel. The doku states: “…Sail provides a great starting point for building a Laravel application using PHP, MySQL, and Redis without requiring prior Docker experience…”. Ok, sounds well. What do I need for this approach?

  • Install Docker desktop for Mac. Done!
  • Install PHP for MAC. I must install PHP in version 7.4 because of dependencies..
    brew install php@7.4
    and no, it didn’t work immediatelly for me. I had to upgrade brew, export some brew stuff, then xcode-select –install and … I finally got it working, but I hate this command line stuff! Done!
  • Install PHP composer for MAC. It’s like CocoaPods but for PHP. For installation just run the commands at https://getcomposer.org/download/ Done!
  • additionally I moved the installed composer to the path, to install it globally and call it on console just as “composer” and not as “php composer.phar”
    sudo mv composer.phar /usr/local/bin/composer

Starting with existing project

After everything is installed, we can start with the PHP-project itself. In my case I had already a laravel project from the friend of mine I mentioned above, however without Sail dependency…So I just opened the project directory and called two commands:

composer require laravel/sail –dev
php artisan sail:install

The latest command failed, because .env file could not be found… hm, in project directory I found .env.example file. So just renamed it to .env, assigned proper parameters and tried installation again. wow, It works!!! It turned out, that .env is the main configuration file in laravel. It should not be pushed to git, only the example version of it.

So, what next?

We have installed sail into existing laravel project. Great. My next questions are:

  • how to use sail stuff?
  • and how can I develop? I mean, which IDE? How can I debug in PHP? And how will code updates get into docker container?

Let’s investigate. Starting and stopping of the docker container with sail is easy. First create an alias:

alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail’

… and then just:

“sail up” in your Terminal => now you can go to Safari and call localhost. Yeeeeh, it works! That means, my local project is running in docker… a kind of magic. Was my project, i.e. php code copied to docker container? I hope, we’ll find it out later.

IDE ?

As for IDE, I decided to try VisualStudioCode for MAC. It makes a good impression and is for free. I found a way how to use Xdebug with Laravel Sail:

https://blog.devgenius.io/xdebug-laravel-sail-project-in-vs-code-b7b73e3dedf7

Now I’m best equipped and am ready for Laravel.

SWIFT: Upload a file with multipart/form-data in iOS using uploadTask and URLSession

Let’s see how to upload any file to a web server, using only Apple’s built-in URLSession. Most of the examples in the Internet utilize dataTask and not the uploadTask. Also the examples I found demonstrate only the uploading of the image files. That’s why I decided to share a working code for building up a multipart/form-data request, that can be used in combination with uploadTask.

// generate boundary string using a unique string
let boundary = UUID().uuidString
        
// Set the URLRequest to POST and to the specified URL
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(yourAuthorizationToken)", forHTTPHeaderField: "Authorization")
        
// Content-Type is multipart/form-data, this is the same as submitting form data with file upload
// in a web browser
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
let fileName = fileURL.lastPathComponent
let mimetype = mimeType(for: fileName)
let paramName = "file"
let fileData = try? Data(contentsOf: fileURL)
var data = Data()
// Add the file data to the raw http request data
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: \(mimetype)\r\n\r\n".data(using: .utf8)!)
        data.append(fileData!)
        data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
// do not forget to set the content-length!
request.setValue(String(data.count), forHTTPHeaderField: "Content-Length")

As you can see, it’s pretty straightforward. First we create a request, setting Content-Type to “multipart/form-data” and Authorization token if required. You can set any other values if you web service requires. The second is to create the data that confirms to the multipart/form-data protocol. For we want to upload any kind of files, the mime type is recognized automatically:

private func mimeType(for path: String) -> String {
        let pathExtension = URL(fileURLWithPath: path).pathExtension as NSString
        guard
            let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, nil)?.takeRetainedValue(),
            let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue()
        else {
            return "application/octet-stream"
        }

        return mimetype as String
    }

So now we have two peaces of puzzle. To bring them together just use the uploadTask(with:data:) function:

session.uploadTask(with: request, from: data)

The webservice should be happy now with your upload file call.

SWIFT: Save NSAttributedString into text file

To save NSAttributedString string into text file we convert first NSAttributedString into Data and afterwards save Data as usual into a text file:

do {
   let rtfData = try attributedText.data(from: .init(location: 0, length: attributedText.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
   try rtfData.write(to: fileURL, options: .atomic)
} catch {
   print(error)
}

SWIFT: Insert substring into string

Working with Strings in Swift is quite different then in Objective-C. Inserting a substring into a string is pretty tricky. Let’s assume following task: given is a html string and we want to extend it by adding some style, i.e. we want to add a string of type <style>/**some style*/</style>
before closing tag </head>. Of course, there is a number of ways how to realize it, e.g JavaScript injection, XML manipulation etc. But let’s see how it can be done using Strings’ operations.

Base idea: we split our HTML string into two parts using String[Range<String.Index>] subscript: before </head> and after </head>. Then we concatenate these parts: part1 + styleString + part2. And this is how it looks in code:

        let styleString = String(format: "<style>%@</style>", cssString)
        var enrichedHtmlString = htmlString
        if let range = enrichedHtmlString.range(of: "</head>") {
            let substringPrefix = enrichedHtmlString[enrichedHtmlString.startIndex..<range.lowerBound]
            let substringSuffix = enrichedHtmlString[range.lowerBound ..< enrichedHtmlString.endIndex]
            enrichedHtmlString = substringPrefix + styleString + substringSuffix
        }

Feel free to contact me if you find a better way to realise inserting substrings into strings in Swift.

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.

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 &amp;&amp; fetchResult.count &gt; 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 &lt;UserNotifications/UserNotifications.h&gt;

Add UNUserNotificationCenterDelegate to the declaration:

#import &lt;UserNotifications/UserNotifications.h&gt;
@interface AppDelegate : UIResponder &lt;UIApplicationDelegate,UNUserNotificationCenterDelegate&gt;
 
@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!

Difference between merge and cherry-pick in git

Generally, cherry picking in git means to choose a particular commit from one branch and apply it onto another. In contrast  merge or rebase apply normally many commits onto another branch.

If you are a console fan and do not use any graphical interfaces working with git, proceed as follow for cherry-pick:

  1. Make sure you are on the branch you want to apply the commit to.

    git checkout master

  2. Execute the following to pick a commit:

    git cherry-pick <commit-hash>

Also note following:

  1. If you cherry-pick from a public branch, you should better use

    git cherry-pick -x <commit-hash>

    This will generate a standardized commit message. This way, you and your co-workers can still keep track of the origin of the commit.

  2. If you have notes attached to the commit they do not follow the cherry-pick. To bring them over as well, You have to use:

    git notes copy <from> <to>

 

More about cherry-pick can be found at git official guide page.