4

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

  • It appears as though `CTCellularPlanProvisioningRequest` is being accessed somewhere, but your wrapper looks correct. You may try an alternative method like a `guard` implementation. I would put a breakpoint in to see if you're getting into the if block under iOS 11.4. Have a peek here: https://www.hackingwithswift.com/new-syntax-swift-2-availability-checking – Adrian Nov 16 '19 at 15:03
  • Thanks for your response. When I comment out the 'offending' lines it works ok on iOS 11.4: `if #available(iOS 12, *) { // let cpProvioningRequest = CTCellularPlanProvisioningRequest() // print("cpProvioningRequest: \(cpProvioningRequest)") } else { print("No iOS 12+ available") }` That produces the following output: `Subscriber: No iOS 12+ available` With the lines in, it does not even start to execute the code, but rather seems to crash when starting. – Frans Glorie Nov 16 '19 at 15:16
  • I can confirm it crashes while it shouldn't. Looks like a framework bug to me. I think you should submit a report to Apple. I've seen similar bugs in the past in other frameworks, e.g. AVFoundation. – mojuba Nov 16 '19 at 16:19
  • Thanks mojuba. That's what I think too. I have now filed a bug with Apple. I will update this thread with their response. I am hoping for a quick remedy or workaround, because my app update is suppose to be released in the next few days. – Frans Glorie Nov 17 '19 at 13:41
  • Added information: I just tested on a physical device (iPhone 5, running iOS 10.3.3) and the app crashes there as well. So at least it's consistent. :-) And still a big issue. – Frans Glorie Nov 18 '19 at 14:26
  • Still waiting for a response from Apple. – Frans Glorie Nov 21 '19 at 22:52

2 Answers2

2

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.

0

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

Kamil.S
  • 5,205
  • 2
  • 22
  • 51
  • Nice one @Kamil, thanks! I like the clever bit that this way the CTCellularPlanProvisioningRequest-call will only be interpreted at runtime, not at compile/build time. A quick test proved that this seems to work ok (not crash) on the iPhone 8/iOS 11.1 simulator that I used. I had to change the code a little (unwrapping before the `init()`-call). In the comment below is the code that worked for me. Let me do further testing and get back to you a.s.a.p. I have not received a response from Apple yet. I posted on the Feedback Assistant site. – Frans Glorie Nov 24 '19 at 18:57
  • `if let classAvailable = NSClassFromString("CTCellularPlanProvisioningRequest") { print("CTCellularPlanProvisioningRequest-class is available: \(classAvailable)") if let cpProvioningRequestType = classAvailable as? NSObject.Type { let cpProvioningRequest = cpProvioningRequestType.init() print("cpProvioningRequest: \(cpProvioningRequestType) \(cpProvioningRequest)") } } else { print("CTCellularPlanProvisioningRequest-class is NOT available") }` – Frans Glorie Nov 24 '19 at 18:57
  • The forced `as!` cast is a bit lazy on my end, however theoretically it can only fail if `CTCellularPlanProvisioningRequest` would become a pure Swift class under some future iOS SDK. Not very likely tbh, but better to be safe than sorry :-) – Kamil.S Nov 24 '19 at 19:08
  • I'm afraid this is a dead-end street after all. I can instantiate a `CTCellularPlanProvisioningRequest`-call like this, but it will be an NSObject. And I can't cast it to `CTCellularPlanProvisioningRequest` or it will cause the same crash as before. Setting a property using `.setValue("www.blah.com", forKey: "address")` is ok, but passing the instantiated class to the `.addPlan` call then causes a compilation error. So as soon as I use `CTCellularPlanProvisioningRequest`, the app crashes on iOS<12 (unless it's surrounded by quotes). Any suggestions? – Frans Glorie Nov 25 '19 at 12:22