Tokuriku
Tokuriku

Reputation: 1352

Very unusual Xcode compile behaviour

Since the release of Xcode 6.1 and iOS 8.1, one of my apps stopped functioning. I managed to reproduce the problem only if I did "RUN" on my device with a scheme of "Release" instead of "Debug".

Now for the problem. This works fine in Debug mode:

import Foundation

class CategoryParser {


    var categoriesSettingsDictionary : [String: AnyObject]?


    init() {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
    }
}

But it crashes in "Release" mode when I instantiate an Object of the CategoryParser type. After many trials and errors, I figured that to stop it from doing the problem I could place the dictionary initialisation between two println() statements. Why would those make any difference?

import Foundation

class CategoryParser {


    var categoriesSettingsDictionary : [String: AnyObject]?


    init() {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        println("_")
        categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
        println("_")
    }
}

Upvotes: 2

Views: 427

Answers (1)

rintaro
rintaro

Reputation: 51911

It must be a bug around optimizations in Swift compiler. I think, it's around bridging NSDictionary to Dictionary<String,AnyObject>.

I reproduced the problem with following setup.


Environment: Xcode 6.1 (6A1052d) / iPhone 6 / iOS 8.1

Template: Single View Application

CategoriesSettings.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>ct1</key>
    <string>test</string>
</dict>
</plist>

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        let result = loadPlist()
        println("result: \(result)")
        return true
    }

    func loadPlist() -> [String: AnyObject]? {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        let dict = NSDictionary(contentsOfURL: categoriesURL!)
        println(dict)
        let result = dict as? [String:AnyObject]
        return result
    }
}

// EOF

outputs (with -O):

Optional({
    ct1 = test;
})
result: nil

outputs (with -Onone):

Optional({
    ct1 = test;
})
result: Optional(["ct1": test])

I don't know the best workaround, though.

Maybe this works:

class CategoryParser {
    var categoriesSettingsDictionary : [String: AnyObject]?
    init() {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
        if categoriesSettingsDictionary == nil {
            // NOTICE: to other developers about this workaround
            println("_")
            println("_")
        }
    }
}

Encapsulating them in autoreleasepool also works:

class CategoryParser {
    var categoriesSettingsDictionary : [String: AnyObject]?

    init() {
        autoreleasepool {
            let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
            self.categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!) as? Dictionary<String, AnyObject>
        }
    }
}

But, as of now, I think, you should use NSDictionary as is, because as long as you only read from it, there is almost no practical difference between NSDictionary and Dictionary<String,AnyObject> in most cases.

class CategoryParser {
    var categoriesSettingsDictionary : NSDictionary?
    init() {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        categoriesSettingsDictionary = NSDictionary(contentsOfURL: categoriesURL!)
    }
}

OR, this may be too aggressive, but you can implement your own NSDictionaryDictionary converter.

extension Dictionary {
    init?(nsDictionaryOrNil:NSDictionary?) {
        if let dict = nsDictionaryOrNil? {
            self = [Key:Value](minimumCapacity: dict.count)
            for (k,v) in dict {
                if let key = k as? Key {
                    if let val = v as? Value {
                        self[key] = val
                        continue
                    }
                }
                return nil
            }
        }
        else {
            return nil
        }
    }
}

class CategoryParser {
    var categoriesSettingsDictionary : [String:AnyObject]?
    init() {
        let categoriesURL = NSBundle.mainBundle().URLForResource("CategoriesSettings", withExtension: "plist")
        let dict = NSDictionary(contentsOfURL: categoriesURL!)
        categoriesSettingsDictionary = [String:AnyObject](nsDictionaryOrNil: dict)
    }
}

Upvotes: 1

Related Questions