简体   繁体   中英

How to include iOS 12+ api call in app while still supporting iOS<12 users?

See the simple code snippet below. For my app I need to include a call to CTCellularPlanProvisioningRequest, part of CoreTelephony. This class is available for iOS 12+. I still have a big user base on iOS 10 and 11 to support, so I enclosed my call in 'if #available`, like so:

if #available(iOS 12, *) {
    let cpProvioningRequest = CTCellularPlanProvisioningRequest()
    print("cpProvioningRequest: \(cpProvioningRequest)")
} else {
    print("No iOS 12+ available")
}

It is ok for me that the call is not executed on iOS<12. However, when I run this code on a simulator device running iOS 11.4 the app crashes at the loading stage. On a simulator running iOS 12 or 13, the call works fine. And when I comment out the two lines with CTCellularPlanProvisioningRequest, the app runs on fine iOS 11.

The error I get on 11.4 is:

dyld: Symbol not found: _OBJC_CLASS_$_CTCellularPlanProvisioningRequest
  Referenced from: /Users/frans/Library/Developer/CoreSimulator/Devices/25E0C601-5D38-4B41-A807-3575BC23AAB9/data/Containers/Bundle/Application/4DC30320-77F6-4107-A83A-EF0F5C5B464D/TestNewAPIOldiOS.app/TestNewAPIOldiOS
  Expected in: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 11.4.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreTelephony.framework/CoreTelephony
 in /Users/frans/Library/Developer/CoreSimulator/Devices/25E0C601-5D38-4B41-A807-3575BC23AAB9/data/Containers/Bundle/Application/4DC30320-77F6-4107-A83A-EF0F5C5B464D/TestNewAPIOldiOS.app/TestNewAPIOldiOS

And my thread view shows:

dyld`__abort_with_payload:
    0x10d2500d4 <+0>:  movl   $0x2000209, %eax          ; imm = 0x2000209 
    0x10d2500d9 <+5>:  movq   %rcx, %r10
    0x10d2500dc <+8>:  syscall 
->  0x10d2500de <+10>: jae    0x10d2500e8               ; <+20>
    0x10d2500e0 <+12>: movq   %rax, %rdi
    0x10d2500e3 <+15>: jmp    0x10d24e601               ; cerror_nocancel
    0x10d2500e8 <+20>: retq   
    0x10d2500e9 <+21>: nop    
    0x10d2500ea <+22>: nop    
    0x10d2500eb <+23>: nop    

My question is: How can I ensure that this call to CTCellularPlanProvisioningRequest works normally on iOS 12+ while not causing a crash or other negative effect on iOS<12?

For completeness, below is the complete class. Deployment target is set to iOS 10. I am aware of the prerequisites for CTCellularPlanProvisioningRequest and in my complete app I have those filled in. I tested on Xcode 11.2.1, as well as Xcode 10. I tried several different device simulators, running various pre-iOS12 versions. I have NOT been able to run this on a physical device running iOS 10 or 11, because I don't have access to one.

//  TestNewAPIOldiOS
//
//  Created by Frans Glorie on 15/11/2019.
//  Copyright © 2019 Frans Glorie. All rights reserved.
//

import UIKit
import CoreTelephony

class ViewController: UIViewController {

    // Functions
    override func viewDidLoad() {
        super.viewDidLoad()

        let ctSubscriber = CTSubscriber()
        print("Subscriber: \(ctSubscriber)")

        if #available(iOS 12, *) {
            let cpProvioningRequest = CTCellularPlanProvisioningRequest()
            print("cpProvioningRequest: \(cpProvioningRequest)")
        } else {
            print("No iOS 12+ available")
        }
    }
}

Thanks!

Frans

I finally received an answer from Apple and what they suggest seems to work: "After reviewing your feedback, we have some additional information for you.

There is a known issue on our side of things, but you can work around this by explicitly weak linking against the framework. To do that, go to build phases, 'link binary with libraries' section, add CoreTelephony there and set the status field from required to optional. That fixes the crash for now. We've filed a separate bug for the issue you mentioned and will fix it in a future release. Thanks! "

I tried this and it seems to work fine. Further testing is needed, but this seems to solve the problem. Hope this helps anyone else.

The workarounds posted here earlier unfortunately all solve only a part of the problem.

Since CTCellularPlanProvisioningRequest is NSObject subclass as a workaround you can instantiate it through obj-c dynamic runtime with failsafe for iOS11 and lower

if let classAvailable = NSClassFromString("CTCellularPlanProvisioningRequest") {
    let cpProvioningRequest = (classAvailable as! NSObject).init()
    print("cpProvioningRequest: \(cpProvioningRequest)")
}

By doing so you will not create dependency in your binary on the _OBJC_CLASS_$_CTCellularPlanProvisioningRequest symbol.

UPDATE
Following OP comments in my answer here's code to invoke addPlan dynamically.

if let requestClassAvailable = NSClassFromString("CTCellularPlanProvisioningRequest"),
    let provisioningClassAvailable = NSClassFromString("CTCellularPlanProvisioning") {
    if let cpProvisioningRequestType = requestClassAvailable as? NSObject.Type,
        let cpProvisioningType = provisioningClassAvailable as? NSObject.Type {

        let cpProvisioningRequest = cpProvisioningRequestType.init()
        let cpProvisinioning = cpProvisioningType.init()

        let addPlanSelector = NSSelectorFromString("addPlanWith:completionHandler:")
        if let methodIMP = cpProvisinioning.method(for: addPlanSelector) {
            unsafeBitCast(methodIMP,to:(@convention(c)(Any?,Selector,Any?,((UInt) -> Void)?)->Void).self)(cpProvisinioning, addPlanSelector, cpProvisioningRequest, nil)   
        }
    }
}

It's far from perfect, but maybe it will suffice until you get an answer from Apple.

Btw I covered in details details of dynamic invoking in Swift in my answer here

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM