3

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.

Charlie Fish
  • 18,491
  • 19
  • 86
  • 179
  • Possible alternative here: https://stackoverflow.com/a/52257828/603977, and AFNetworking had a similar issue which has a possible resolution using a post_install hook: https://stackoverflow.com/questions/28987586/how-to-include-afnetworking-as-a-framework-for-using-in-an-ios-app-and-extension/29335471#29335471 – jscs Feb 19 '19 at 19:44
  • @JoshCaswell That second link looked more promising but did not work. The first link seems REALLY hacky, but might end up being a decent solution. You lose all the safety of the compiler and such by using the first solution. – Charlie Fish Feb 19 '19 at 20:29
  • Have you check this? https://stackoverflow.com/questions/42114122/using-cocoapods-in-an-app-extension-using-a-framework – Sagar Chauhan Feb 26 '19 at 06:49
  • @SagarChauhan I don’t think that is the same thing. – Charlie Fish Feb 26 '19 at 06:51
  • That framework it is using, can't you fork it and add IF_EXTENTION? – MCMatan Feb 27 '19 at 18:40
  • @MCMatan I wrote `MyCustomSwiftPackage`, it's a custom private CocoaPod. Of course names changed. But I have complete control over all the source code in this example/problem. The problem is, I can't figure out how to use one codebase cleanly for both the app and extension while using `UIApplication.shared`. – Charlie Fish Feb 27 '19 at 18:42
  • @CharlieFish what for you using `UIApplication.shared`? Can you use something else? – ManWithBear Mar 26 '19 at 11:21
  • @ManWithBear Mainly to get the top view controller. One other dependency relies on it too. I also think there are a few other use cases that I forget off the top of my head. – Charlie Fish Mar 26 '19 at 12:05
  • @CharlieFish You can wrap this functionality in some `protocol TopViewControllerProvider`, that in app will search normally and in extension just return extension controller. And for `topViewController` just `window` should be enough, no? – ManWithBear Mar 26 '19 at 12:08
  • Ok but how do I have two different versions, one for the main app and one for the extension? That MIGHT work, but I think the question is still valid for how to separate that code out in an easy way. – Charlie Fish Mar 26 '19 at 12:14
  • @CharlieFish check my answer – ManWithBear Mar 26 '19 at 12:37
  • @CharlieFish sorry I was not available for some time. Reading you question again, why don’t you just inject Shared Application from out side (your custom pod) instead of accessing Shared Application directly – MCMatan Mar 29 '19 at 07:42
  • @MCMatan I just prefer not to do that. Doesn’t feel like a clean solution and not really how I’m thinking about this project. Just doesn’t feel like the cleanest solution to me or what I wanna do. – Charlie Fish Mar 29 '19 at 14:39
  • Mmm if pods context is dynamic, it seems clean to inject it on pods initial load – MCMatan Mar 29 '19 at 14:51

2 Answers2

2

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!

MCMatan
  • 8,623
  • 6
  • 46
  • 85
  • 1
    Awesome. Then I can just use `#if MYLIBRARY_APP_EXTENSIONS` in my code base to check if it's in an extension? – Charlie Fish Feb 27 '19 at 19:07
  • Yes, exactly (; – MCMatan Feb 28 '19 at 07:37
  • 1
    I haven't gotten a chance to test this yet. I will award you the bounty if I don't get a chance to test before the award time expires, and if there is no better answer in the meantime. Once I actually get around to testing it I will accept and upvote. I think it should work perfectly tho. – Charlie Fish Mar 01 '19 at 15:47
  • Glad I could help (: – MCMatan Mar 01 '19 at 20:12
  • I'm getting a weird compile error that `No such module 'MyLibrary'` in my app extension. It doesn't look like it's effecting my main app target. – Charlie Fish Mar 10 '19 at 16:37
  • In your example you use `MyLibrary`. So I was just saying that to keep it consistent with your example. It's on the line `import MyLibrary`. My actual package name is different, but I'm calling it `MyLibrary` here just to keep it consistent. – Charlie Fish Mar 10 '19 at 16:44
  • I guess one other thing I should ask. I put `s.source_files` instead of putting it in each `subspec` since it's the exact same between the two subspecs. I'm not sure if that is causing problems. I don't want to duplicate that `source_files` twice. – Charlie Fish Mar 10 '19 at 16:46
  • So yes, you have to have `source_files` duplicated twice. If you have any suggestions for how to fix that, that'd be awesome. Also I had to use `ext.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-DIN_EXTENSION' }` instead of your way. If you want to edit your answer I'll up vote and accept. – Charlie Fish Mar 10 '19 at 17:25
  • Sorry I could not help with the source_files duplication, I'll do some research about it, never tried to deal with it myself, it is a good point... About the preprocessor flag you have used, did you use (According to example) `ext.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => 'MYLIBRARY_APP_EXTENSIONS=1' }`? If not, could you please explain what is `-DIN_EXTENSION` – MCMatan Mar 10 '19 at 17:40
  • It was meant to fix compile errors. `IN_EXTENSION` is the same as `MYLIBRARY_APP_EXTENSIONS`. https://medium.com/@maxcampolo/swift-conditional-logging-compiler-flags-54692dc86c5f. The problem I'm running into now, is even in the core application it is still using that `IN_EXTENSION` flag. It looks like the main app Pods framework is has both the `AppExtension` and `Core` linked in the Target Dependancies. – Charlie Fish Mar 10 '19 at 23:45
  • It looks like CocoaPods is embedding every pod installed in the extensions in the main app as well. So the main app has both `Core` and `AppExtension`. And `AppExtension` looks to be taking priority with the `OTHER_SWIFT_FLAGS`. So the conditional `#if` statements aren't working. – Charlie Fish Mar 11 '19 at 00:00
0

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
        ....
    }
}
ManWithBear
  • 2,787
  • 15
  • 27
  • I’d prefer a solution that is based mainly in the Pod and not the client app. Also rootViewController is not the same as the top view controller in the stack. – Charlie Fish Mar 28 '19 at 05:31
  • @CharlieFish you need some start controller for your `topViewController` algorithm. I show how to get this start point – ManWithBear Mar 28 '19 at 08:29
  • Makes sense. But seems like there should be a way to do this without requiring that in the client app. It should all be within the Pod. – Charlie Fish Mar 28 '19 at 15:32
  • @CharlieFish on other hand, you trying to put inside Pod code, that will be used only by one specific client, and not used by others. Is it make sense? I would say it is project specific. So your call – ManWithBear Mar 28 '19 at 15:34
  • I don’t understand what you mean. The other answer looks really promising. Because it’s all within the Pod basically. But it doesn’t work. – Charlie Fish Mar 28 '19 at 15:35
  • @CharlieFish I mean it will be better if Pod contains code that used by all platforms. And code that used only on one platform should be extracted either in app or another Pod – ManWithBear Mar 28 '19 at 15:49