Migration of an iOS app to Xcode 8 Swift 3 and iOS 10

Peter FennemaAWS, iOS, Swift9 Comments

In this blog post I will describe my experiences of migrating a Swift 2.2 app to Swift 3 after installing Xcode 8. I will describe the issues that I run into, as well as the way I am going to solve them. I have written most parts of this blog post chronologically while I was going through the process. If you are in a hurry you can skip to lessons learned immediately.

Every app is different, and every migration will be different too. This blog post is specific to my app, but it gives you at least some idea of the amount of work, the steps involved, and the kind of issues you might run into. Useful info if you need to do migrations too. My app has about 160 source files and uses 2 external frameworks. The app also uses CoreData and accesses a web service. My app has iOS 9.3 as deployment target.

First thing to notice is that a migration is required in order to continue developing in Xcode 8. You can choose between Legacy Swift (2.3) or Swift 3. I will go for the Swift 3 conversion.

Here’s an overview of the issues that I encountered

Prerequisites

Before starting the conversion you need to check your dependencies with external frameworks. You have to make sure that these frameworks also support Swift 3. If this is not the case you are kinda stuck. Best thing to do is to request the developers of these frameworks to create Swift 3 versions.

In my app I use the Amazon Web Services AWS-SDK-iOS (version 2.4.1) and Kingfisher (version 2.4.3). AWS-SDK-iOS is an Objective-C framework. I do not expect that I need an updated version. Let’s hope that the Objective-C bridging works well after migrating. Kingfisher is a Swift framework. A Swift 3 version has already been released. Great job of the Kingfisher developers. I use Kingfisher for image downloading and caching.

I use Carthage for dependency management and Git for version control.

Step 1: Code Migration

When opening the project Xcode asks: “Convert to Current Swift Syntax?”
After clicking “Convert” you can choose the version (2.3 or 3). I will pick version 3.

The migration tool is running for about one minute. It has updated 89 files. It is possible to review the changes before saving them, but I save immediately. I will use Xcode with Git to review the changes.

Result of Step 1: Xcode reports 4 error messages, all related to Kingfisher not being compatible to Swift 3.

Step 2: Upgrading Kingfisher

First I modify the Kingfisher version in the Cartfile to a Swift 3 compatible version (github “onevcat/Kingfisher” ~> 3.0) and run the update command (carthage update Kingfisher). The downloaded version is 3.1.0. I clean build the project.

Result of Step 2:
No more Kingfisher error messages 🙂
The reward is that I have 11 new errors 🙁

Issue 1: NSFetchRequest errors

In the code snippet below the lines with errors are marked.
Line 1: Type ‘AnyObject’ does not conform to protocol ‘NSFetchRequestResult’
Line 2: Generic parameter ‘ResultType’ could not be inferred

Before the migration the code looked like this:

Line 1: The issue here is that NSFetchRequest is now a generic class. Therefore we need to specify a type for NSFetchRequest. The migrator has done a best effort and has added AnyObject as a type. This causes a compilation error, because the type has to conform to protocol NSFetchRequestResult. We can solve that by replacing AnyObject by NSManagedObject. In this case however, I can be more specific, because I know that the entity being returned when executing the fetch request is a Notification. So I replace AnyObject with Notification. Notification is a subclass of NSManagedObject. Here Notification is a concept of my own app, don’t confuse this with the newly introduced Foundation.Notification in iOS10.

Line 2: When creating a NSFetchRequest we need to specify a type. Again we use type Notification

The errors in line 1 and 2 of this code snippet are fixed now. The result is shown below.

I thought I was done with this snippet, but now, having solved the previous errors, a new one shows up! 🙁

Issue 2: NSDate vs Date

What’s going on? The migration tool has replaced NSDate by Date in line 1. This causes an error at line 6: Argument type ‘Date’ does not conform to expected type ‘CVarArg’

The error provides a Fix-it: ‘Insert as CVarArg’. I can apply the Fix-it blindly and I am done. But I always feel a bit uneasy when I apply a fix that I do not understand. And I do not like this CVarArg ‘thing’. So, what’s going on here? It looks like passing a Date object to a predicate causes issues. I cast the Date to an NSDate to solve the issue.

The resulting code is shown below. The good news is: This snippet compiles! 🙂

Well, 3 errors in 9 lines of code. it’s going to be a long day (week?). This is not the only snippet with these errors. So I have fixed them everywhere in my code. I will continue the migration process by picking some new errors that appeared after solving the previous ones.

Issue 3: Extraneous argument labels in call

The errors are in line 6, 15 and 19: Extraneous argument labels ‘currentUser:error:’ in call
The provided Fix-it suggests to remove the labels from the completion function.

Code before migration

Before jumping to conclusions and blindly accept the Fix-it, let’s take a look at the original code before the migration.You’ll notice that there are a lot of differences. I will walk through them later, but let’s focus on the error first. By comparing line 1 for both versions you can see that the signature of the function was changed. An underscore was added in front of all labels. This means that this function must be called without argument label. This explains the error message. The completion handler is called with the labels. This confirms the solution suggested by the Fix-it to remove the labels, and I will apply it.

The function compiles without errors. Here’s the result:

After applying the same Fix-it in other locations Xcode reports some new errors. Before continuing to the next error I will take a look at the changes that the migration tool has made to this function in more detail. These changes do not require any manual action, but it is interesting to observe what exactly is done by the migration tool.

Observation 1: Some argument labels get ‘_’

In Swift 3 all function parameters have labels, unless specified otherwise. In Swift 2 functions did not require a label for their first parameter. To solve this incompatibility the migration tool adds a ‘_’ in front of argument labels of functions where needed.

Observation 2: Closure is prefixed with @escaping

In Swift 1 and 2 closures were escaping by default. In Swift 3 closures are non-escaping by default. The migrator figured out that the completion closure is escaping and adds the @escaping flag. Why is the completion closure ‘escaping’? Because the function calls an async remote service and returns immediately. The completion closure is called later, after the function has returned, it has ‘escaped’ from the function. There’s an excellent explanation about (non)escaping closures in Swift 3 on swiftunboxed.

Observation 3: Enums are now LowerCamelCase

In line 4 Unknown was changed to unknown

Observation 4: New api for Grand Central Dispatch

An example can be seen in lines 5, 14, 18, where dispatch_async(dispatch_get_main_queue(), {…}) was replaced by DispatchQueue.main.async(execute: {…})

Issue 4: CAAnimationDelegate

The next error leads me to an animation related issue. When opening the file there are actually 2 errors:
Line 5: Cannot assign value of type ‘PulsingCircularLoaderView’ to type ‘CAAnimationDelegate?’
Line 9: Method does not override any method from its superclass

The original code:

The difference is that the migration tool has added an ‘_’ in front of the anim parameter. Removing the ‘_’ does not solve the issue. What’s going on here? The class directly inherits from UIView. Did Apple make a breaking change in UIView? The interesting thing is that I can not find animationDidStop(..) in the Api documentation of UIView.

Let’s take a look at the error in line 5 first. The animationGroup.delegate expects a CAAnimationDelegate type. This also provides a hint for the other error. Time to jump into the api documentation to take a look at CAAnimationDelegate. Yes indeed, the function animationDidStop(..) is part of the CAAnimationDelegate protocol. We can solve the issue in line 5 by making PulsingCircularLoaderView conform to CAAnimationDelegate

Below is the result after the fixes. PulsingCircularLoaderView conforms to CAAnimationDelegate now (line 1). In line 9 we remove the ‘override’ keyword. If everything goes well this function is called via the delegate pattern instead of via inheritance as before. The function now compiles, but there are some potential runtime issues.

I can not test yet if the animationDidStop(..) is really called. CAAnimationDelegate is introduced since iOS 10. Will this code work on iOS9? (a requirement for my app). There are still too many other compilation errors to check this out, so I will add a // TODO: Migration to the code and revisit when I can really test at runtime.

Update: This fix works for both iOS 9 and iOS 10

Issue 5: NSNumber conversion

Error in line 6: Argument labels ‘(_:)’ do not match any available overloads

Before migration

The variable coreDataPosition is of type NSNumber. The code before migration provided an implicit conversion from newValue (Int) to coreDataPosition (NSNumber). The migration result creates an NSNumber object. Unfortunately it does not generate a correct initialiser call. Numbers have to be initialized with a value label in Swift 3. So the fix is simply adding the correct initialiser.

After migration with error fixed

This one was easy 🙂

Issue 6: Error parameter removed in NSManagedObject count(..)

This code was not touched by the migration tool.

Code with error in line 4: Extra argument ‘error’ in call

The count function can no longer be called with an error parameter. The solution is to apply try..catch instead.

Code after applying fix

Issue 7: NSURL vs URL

Error in line 3: Initializer for conditional binding must have Optional type, not ‘[String]’

Before migration

In line 1 the migration tool has replaced NSURL by URL. By comparing the api docs for both, I noticed that for NSURL pathComponents returns an optional, and for URL pathComponents returns a non-optional. This explains the error. This can be solved by simply removing the check let pathComponents = url.pathComponents and replacing the guard by an if.

Code with error fixed

Issue 8: Objective-C bridging issue with AWS SDK

This error occurs in the code that uses the AWS-SDK-iOS to access a web service.

Error in line 2: Ambiguous use of ‘continue’

Before migration

The migration tool has renamed task.continueWithBlock to task.continue. It’s a bit of a mystery to me what’s going on here. The task is of type AWSTask, a concept provided by the AWS-SDK-iOS. It is written in Objective-C. So, calling continue on an AWSTask is ambiguous. I was not able to discover why. Undoing the migration change did’t work either. I noticed that my code uses a trailing closure. Why not try to write the closure within the function call’s parentheses? And lucky enough this has solved the ambiguity. I needed to apply this fix in about 15 locations.

Code with error fixed

Issue 9: Objective-C NSError converted to Error

This error appeared after I solved the previous error, in the same code snippet:

Error in line 5: Cannot convert value of type ‘Error’ to expected argument type ‘NSError?’

Xcode offers a Fix-it in line 5: insert “as NSError?”. Applying the fix-it solves the issue. Still, it is interesting to know a bit more about the cause of this compiler error.

The variable resultTask type is AWSTask, provided by the Objective-C AWS-SDK-iOS. In the Objective-C api resultTask.error is of type NSError?. Inspection of the Swift type in line 5 shows that resultTask.error is of type Error. The bridging from Objective-C to Swift has modified the type NSError into Error. There is a documented rationale behind this. To be honest, I want to finish my migration, so I won’t read it in detail, but I did take a quick look at it and it confirms the mapping of NSError to Error.

We have an explanation for the compiler error now. The type of error in line 5 is Error? . The completion closure in line 1 requires a NSError? . We can safely cast the error to NSError? as suggested by the Fix-it, because we know that the Objective-C library uses NSError.

Issue 10: Protocol conformance and @escaping

In my app I have a protocol ViewControllerRefreshDelegate with an implementation in AppDelegate

Code after migration

The compiler error: Type ‘AppDelegate’ does not conform to protocol ‘ViewControllerRefreshDelegate’. I have solved this by adding @escaping to the protocol definition (in line 2)

Issue 11: Change in UIViewControllerTransitioningDelegate protocol

Error in line 2: Incorrect argument label in call (have ‘presentedViewController: presentingViewController:’, expected ‘presentedViewController:presenting:’)

Code before migration

There has been a change in the api and Xcode offers a Fix-it: Replace “presentingViewController” with “presenting”. After the fix the function compiles, but there is a little caveat.

After the fix

Before the migration the presenting parameter was a non optional UIViewController. After the migration it suddenly is optional. By comparing line 2 of the code before and after migration you can see that the migration tool has force-unwrapped the presenting viewcontroller by adding an exclamation mark. I am wondering if this is safe in all circumstances. I am focusing on compilation errors now, but I will mark this issue with a // TODO: Migration and revisit when all compile errors are solved and I can run the app.

Issue 12: NSNotification vs Notification

Errors in lines 5, 7: Expression pattern of type ‘String’ cannot match values of type ‘Notification.Name’ (aka ‘NSNotification.Name’)

Code before migration:

The migration tool has replaced NSNotification with Foundation.Notification. There is a compiler error because notification.name has type Name, and ScanningStartedNotification has type String. We can solve this by replacing switch notification.name with switch notification.name.rawValue. Now we are comparing strings again.

Issue 13: Kingfisher errors

The remaining errors are all related to changes in Kingfisher. These errors are caused by the way I have integrated this framework. I would rather fix this when the app runs. Therefore I will comment out the Kingfisher code to make the app compile. My app can run without downloading and caching images. I consider solving Kingfisher integration issues to be out of scope for this blog post.

The compile errors all have been fixed now. Unfortunately there are some runtime errors as well.

Issue 14 (runtime): Crash while presenting UIViewController

After initializing my app is supposed to present a modal viewcontroller with a nice animation. But it crashes at the point of presentation. This appears to be caused by the caveat that I described in issue 11. The migration tool has force-unwrapped the presenting viewcontroller by adding an exclamation mark. The crash is the “famous” ‘unexpectedly found nil while unwrapping an Optional value’ at exactly this point. I solve this error by removing the exclamation mark.

Issue 15 (runtime): Keychain security error on iOS 10

The AWS-SDK-iOS saves Cognito identity data of the user in the keychain. At runtime I noticed that in some cases identity information is not saved, and the logs show a security error with code 34018. The error leads to very ugly bugs, that render my app useless. After some investigation I noticed that in my case this error only appears in iOS 10. This issue is haunting many developers. It exists already for a long time, in a diversity of scenarios and circumstances. Apple is doing a lot to solve it, but it is a complex issue that is hard to reproduce systematically. You can find more info about it at the developer forum.

Fortunately there is a solution: You can add a KeyChain Entitlement. Go to your project target and select Capabilities -> Keychain Sharing -> Add Keychain Groups+Turn On. This solves the issue. I found this on Stackoverflow.

Lessons learned

I had around 100 compile errors, tracked back to 13 compile time issues. Until now I have found 2 fatal runtime issues. One runtime issue only manifests itself on iOS 10.

The migration potentially has a large impact on your app. The migration tool does a lot of work for you, but it will introduce issues that you have to fix. Some of them are very easy, others require some more effort. Once the compile errors are fixed you might encounter some runtime errors too. This is the most tricky aspect of the migration. You do have to test your app really well.

9 Comments on “Migration of an iOS app to Xcode 8 Swift 3 and iOS 10”

  1. Concerning AWSTask continue issue, I think things are getting confused

    continue(block: (AWSTask) -> Any?)
    continue(successBlock: (AWSTask) -> Any?)

    So, which one is used? With no name being provided
    task.continue({ (resultTask) -> AnyObject? in
    ....
    })

    I don’t know, I tried
    task.continue(block: { (resultTask) -> AnyObject? in
    ....
    })

    But it didn’t help.

    1. The Objective-C signatures in AWSTask.h are:

      - (AWSTask *)continueWithBlock:(AWSTask<ResultType> *task)block;
      - (AWSTask *)continueWithSuccessBlock:(AWSTask<ResultType> *task)block;

      The Swift 3 signatures (bridged from objective-C):

      open func continue(_ block: (AWSTask<ResultType>) -> Any?) -> AWSTask<AnyObject>
      open func continue(successBlock block: (AWSTask<ResultType>) -> Any?) -> AWSTask<AnyObject>

      Specifying no label will call the first function. Note the ‘_’ in the first function signature. Because of this ‘_’ you do not need to specify the ‘block’ label. Specifying the ‘block’ label will produce a compilation error.

      To call the second function you do need to specify the label ‘successBlock’ explicitly.

  2. Hi Peter,

    Thank your for your reply, basically it’s declared in AmazonClientManager like so :

    var completionHandler: AWSContinuationBlock?

    1. This is the declaration of the AWSContinuationBlock (I use AWS-SDK-iOS 2.4.9):
      public typealias AWSContinuationBlock = (AWSTask<ResultType>) -> Any?
      It uses a type parameter: ResultType

      Now declare the completionHandler in AmazonClientManager as follows:
      var completionHandler: ((AWSTask<AnyObject>) -> Any?)?
      I replaced the type parameter ResultType of AWSContinuationBlock by an actual type: AnyObject

      I expect that it will compile now (it does in my own codesnippet).

      1. Thank you for your insight, I actually am using AWSTask.h – in it it’s declared like so:

        typedef __nullable id(^AWSContinuationBlock)(AWSTask *task);

        I used the typealias as suggested by the SDK and then I declared the var off of your suggestion and it worked great! Not declaring the typealias breaks the code in other functions which take AWSContinuationBlock as parameters. However, I’m unsure why AWSTask in this other function is not being resolved – it has to do with the AWSTask array. This is not in AmazonClientManager but in CognitoDatasetListViewController

        @IBAction func refreshClicked(_ sender: AnyObject) {
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
        //var tasks = [AWSTask]()
        //var task: AWSTask?
        var tasks: [AWSTask]?

        for dataset in self.datasets {
        tasks?.append( AWSCognito.default().openOrCreateDataset(dataset.name).synchronize())
        //tasks.append(AWSCognito.default().openOrCreateDataset(dataset.name).synchronize())
        }
        AWSTask(forCompletionOfAllTasks: tasks).continue ({ (task: AWSTask) -> AnyObject? in
        return AWSCognito.default().refreshDatasetMetadata()
        }).continue({ (task: AWSTask) -> AnyObject? in >>>>> “Cannot invoke continue with an argument list of type ((AWSTask) -> Any?)
        DispatchQueue.main.async {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false

        if task.error != nil {
        self.errorAlert(task.error!.description)
        } else {
        self.datasets = AWSCognito.default().listDatasets()
        self.tableView.reloadData()
        }
        }
        return nil
        })
        }

        1. My apologies this is the updated one.

          @IBAction func refreshClicked(_ sender: AnyObject) {
          UIApplication.shared.isNetworkActivityIndicatorVisible = true
          //var tasks = [AWSTask]()
          //var task: AWSTask?
          var tasks: [AWSTask]?

          for dataset in self.datasets {
          tasks?.append( AWSCognito.default().openOrCreateDataset(dataset.name).synchronize())
          //tasks.append(AWSCognito.default().openOrCreateDataset(dataset.name).synchronize())
          }

          AWSTask(forCompletionOfAllTasks: tasks).continue ({ (task: AWSTask) -> AnyObject? in
          return AWSCognito.default().refreshDatasetMetadata()
          }).continue({ (task: AWSTask) -> AnyObject? in
          DispatchQueue.main.async {
          UIApplication.shared.isNetworkActivityIndicatorVisible = false

          if task.error != nil {
          self.errorAlert(task.error!.description)
          } else {
          self.datasets = AWSCognito.default().listDatasets()
          self.tableView.reloadData()
          }
          }
          return nil
          })

          }

          1. I was able to fix it, by changing the line to this

            .continue(successBlock: { (_task:AWSTask) -> AnyObject? in

  3. I am going through the same thing and I would like to applaud your migration journey 🙂 albeit painful 🙂 I was wondering if you would have any suggestion on this?

    func completeLogin(_ logins: [AnyHashable: Any]?) {
    var task: AWSTask?

    if self.credentialsProvider == nil {
    task = self.initializeClients(logins)
    print(“task->” + String(describing: task))
    } else {
    credentialsProvider?.invalidateCachedTemporaryCredentials()
    task = credentialsProvider?.getIdentityId() as! AWSTask?
    print(“task->” + String(describing: task))
    }
    task?.continue ({
    (task: AWSTask!) -> AnyObject! in
    if (task.error != nil) {
    let userDefaults = UserDefaults.standard
    let currentDeviceToken: Data? = userDefaults.object(forKey: Constants.DEVICE_TOKEN_KEY) as? Data
    var currentDeviceTokenString : String

    if currentDeviceToken != nil {
    currentDeviceTokenString = currentDeviceToken!.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength64Characters)
    } else {
    currentDeviceTokenString = “”
    }

    if currentDeviceToken != nil && currentDeviceTokenString != userDefaults.string(forKey: Constants.COGNITO_DEVICE_TOKEN_KEY) {

    AWSCognito.default().registerDevice(currentDeviceToken).continue ({ (task: AWSTask!) -> AnyObject! in
    if (task.error == nil) {
    userDefaults.set(currentDeviceTokenString, forKey: Constants.COGNITO_DEVICE_TOKEN_KEY)
    userDefaults.synchronize()
    }
    return nil
    })
    }
    }
    return task
    }).continue(self.completionHandler!) >>>>>>> This throws error : Cannot convert value of type ‘AWSContinuationBlock’ (aka ‘(AWSTask) -> Optional’) to expected argument type ‘(AWSTask) -> Any?’

    Would you have any idea as to how to carry on with the .continue command here? I realize it used to be .continueWithBlock …

Leave a Reply

Your email address will not be published. Required fields are marked *