简体   繁体   中英

Sharing CocoaPod using UIApplication.shared between iOS Extension and app

I have a custom built private CocoaPod that I wrote. I'm trying to use it in my iOS application, which is working fine. But when I add it to my iMessage App or Share Extension it fails and gives me an error 'shared' is unavailable: Use view controller based solutions where appropriate instead. when trying to use UIApplication.shared .

My first thought of how to fix this was to add a Swift Flag IN_EXTENSION or something like that. Then wrap the code in an #if block.

Problem is the target for the CocoaPod source is in some type of framework. The source is not part of the app or extensions directly. So adding that flag doesn't really help.

Below is an example of my Podfile.

source 'https://github.com/CocoaPods/Specs.git'
source 'git@github.com:CUSTOMORG/Private-CocoaPods-Spec.git'
platform :ios, '9.0'
use_frameworks!
inhibit_all_warnings!

target 'MyApp' do
  pod 'MyCustomSwiftPackage', '1.0.0'
end

target 'MyApp Share Extension' do
  pod 'MyCustomSwiftPackage', '1.0.0'
end

If I comment out the line pod 'MyCustomSwiftPackage', '1.0.0' under MyApp Share Extension it works fine. But if I leave it uncommented it fails.

I do need this package in my share extension tho.

I've thought about writing a separate pod that just handles the UIApplication.shared logic and adding that pod to the MyApp . But that seems like a real pain. Especially since I'm not aware of a way to deploy 2 CocoaPods in 1 project that rely on the same source files.

If that is the only solution it almost seems better to use Git Submodules and have the source directly in the app, so I can have it part of those targets directly and the #if SHOULD work then. Problem with that is the dependancies of the CocoaPod wouldn't be handled if I use Git Submodules. So I really have to use CocoaPods somehow.

I'd prefer a simple solution that doesn't feel as hacky as those ones. So is there a better way to handle this and fix that error without resorting to rewriting a TON of code, and that isn't a super hacky solution?


In the comments it was mentioned to use NSSelectorFromString with UIApplication.responds and UIApplication.perform . Problem with that is if Apple ever changes the API, the code will break, even for previous versions of the application since it is being called dynamically with no API future proofing. Although that solution sounds easy, it seems like a really bad decision.


The answer below looks very promising. Sadly after a few changes outlined in the comments, it still isn't working, with the main application having both the Core subspec along with the AppExtension subspec.

Say you're owner of MyLibrary :

Pod::Spec.new do |s|
  s.name             = "MyLibrary"
  # Omitting metadata stuff and deployment targets

  s.source_files = 'MyLibrary/*.{m,h}'
end

You use unavailable API, so the code conditionally compiles some parts based on a preprocessor macro called MYLIBRARY_APP_EXTENSIONS . We declare a subspec, called Core with all the code, but the flag off. We make that subspec the default one if user doesn't specify one. Then we'll declare an additional subspec, called AppExtension including all the code, but setting the preprocessor macro:

Pod::Spec.new do |s|
  s.name             = "MyLibrary"
  # Omitting metadata stuff and deployment targets
  s.default_subspec = 'Core'

  s.subspec 'Core' do |core|
    core.source_files = 'MyLibrary/*.{m,h}'
  end

  s.subspec 'AppExtension' do |ext|
    ext.source_files = 'MyLibrary/*.{m,h}'
    # For app extensions, disabling code paths using unavailable API
    ext.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'MYLIBRARY_APP_EXTENSIONS=1' }
  end
end

Then in your application Podfile you'll link against Core in your main app target, and against AppExtension in your extension, like so:

abstract_target 'App' do
  # Shared pods between App and extension, compiled with same preprocessor macros
  pod 'AFNetworking'

  target 'MyApp' do
    pod 'MyLibrary/Core'
  end

  target 'MyExtension' do
    pod 'MyLibrary/AppExtension'
  end
end

That's it!

Since you need access to UIApllication.shared only to get topViewController . You can't make it as dependency of your framework, that user needs to provide.

Let's declare provider and with precondition ensure that developer will not forget to setup this property:

protocol TopViewControllerProvider: class {
    func topViewController() -> UIViewController
}

enum TopViewController {
    static private weak var _provider: TopViewControllerProvider?
    static var provider: TopViewControllerProvider {
        set {
            _provider = newValue
        }
        get {
            precondition(_provider != nil, "Please setup TopViewController.provider")
            /// you can make provider optional, or handle it somehow
            return _provider!
        }
    }
}

Then in your app you could do:

class AppDelegate: UIApplicationDelegate {
    func applicationDidFinishLaunching(_ application: UIApplication) {
        ...
        TopViewController.provider = self
    }
}
extension AppDelegate: TopViewControllerProvider { ... }

In extension you can just always return self.

Another way to get topViewController by any view:

extension UIView {
    func topViewController() -> UIViewController? {
        /// But I not sure that `window` is accessible in extension
        let root = window?.rootViewController
        ....
    }
}

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