0

The answer here: https://stackoverflow.com/a/51250282/1343140 says that it should be possible to have a lazy var be marked @available to higher a iOS version than used at runtime.

I am working on some code where if the user is on iOS 13 their data is encrypted (because it's stored in the cloud). In iOS 12 their data is stored locally and not encrypted.

Here is a simplified version of what I am doing:

import CryptoKit
import Foundation

class DataStore {

    @available(iOS 13.0, *)
    fileprivate lazy var crypto = Crypto()

    func store(data: Data) {
        let url = URL(fileURLWithPath: "myfile.dat")
        if #available(iOS 13.0, *) {
            try! crypto.encrypt(data: data).write(to: url)
        } else {
            try! data.write(to: url)
        }
    }

}

@available(iOS 13.0, *)
class Crypto {
    // SymetricKey is only available in iOS 13. In reality we may load this from keychain
    lazy private var key: SymmetricKey = SymmetricKey(size: .bits256)

    func encrypt(data: Data) -> Data {
        // do encrpyion
        return data
    }
}

let store = DataStore()
store.store(data: "hello data".data(using: .utf8)!)

This compiles fine, and works well in iOS 13.

However, in iOS 12 I see the following crash at runtime when let store = DataStore() is called:

dyld: lazy symbol binding failed: can't resolve symbol _$s9CryptoKit12SymmetricKeyVMa in [...] because dependent dylib #7 could not be loaded
dyld: can't resolve symbol _$s9CryptoKit12SymmetricKeyVMa in [...] because dependent dylib #7 could not be loaded

I would like not to have to load the Crypto class every time the store function is called, because there is significant overhead (reading from keychain), but I can't figure out how to make Crypto a property so it stays in memory for iOS 13 AND is not loaded at all in iOS 12.

Is this possible? If not, what would be the best way to approach this? Make Crypto a singleton?!

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
lewis
  • 2,936
  • 2
  • 37
  • 72

2 Answers2

0

You need to weakly link the CryptoKit framework to your project. Even if the codepath accessing CryptoKit is not executed, you have an import statement in your file that is executed on older iOS versions too.

Weak linking resolves this issue. For more information on weak linking, see the official documentation of WeakLinking.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Hi David, thanks for this. I couldn't get weak linking to work with `lazy var`. Quite possibly I am doing something wrong – lewis Jun 12 '20 at 09:01
  • @lewis no code changes should have been necessary if you implemented weak linking correctly. Weak linking should only change the project settings. – Dávid Pásztor Jun 12 '20 at 09:24
0

I added the weak linking recommended by Dávid but I still saw the same issue.

In my case I switched to a singleton which allows the key to stay in memory but means it can be explicitly avoided at runtime.

import CryptoKit
import Foundation

class DataStore {

    /* ❌ */
    // @available(iOS 13.0, *)
    // fileprivate lazy var crypto = Crypto()

    func store(data: Data) {
        let url = URL(fileURLWithPath: "myfile.dat")
        if #available(iOS 13.0, *) {
            /* ❌ */
            // try! crypto.encrypt(data: data).write(to: url)

            /* ✅ */
            try! Crypto.shared.encrypt(data: data).write(to: url)

        } else {
            try! data.write(to: url)
        }
    }

}

@available(iOS 13.0, *)
class Crypto {

    // 
    static var shared = Crypto()
    private init() {}
    // 

    // Symetric key is only available in iOS 13
    lazy private var key: SymmetricKey = SymmetricKey(size: .bits256)

    func encrypt(data: Data) -> Data {
        // do encrpyion
        return data
    }
}

let store = DataStore()
store.store(data: "hello data".data(using: .utf8)!)
lewis
  • 2,936
  • 2
  • 37
  • 72