在iOS扩展和应用程序之间共享使用UIApplication.shared的CocoaPod

3
我有一个自定义的私有CocoaPod,我写过它。我正在尝试在我的iOS应用程序中使用它,这很好用。但是当我将它添加到我的iMessage应用程序或共享扩展时,它会失败并给出一个错误:'shared' is unavailable: Use view controller based solutions where appropriate instead. ,尝试使用UIApplication.shared时。

我的第一个想法是添加一个Swift标志IN_EXTENSION或类似的东西。然后将代码包装在#if块中。
问题是CocoaPod源的目标是某种类型的框架。源不是应用程序或扩展的直接部分。因此,添加该标志并没有真正帮助。
下面是我的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

如果我在“我的应用程序共享扩展”下的 pod 'MyCustomSwiftPackage',‘1.0.0’ 这一行添加注释,则可以正常工作。但如果我不注释它,则会失败。
然而,我确实需要在我的共享扩展中使用这个包。
我考虑编写一个单独的pod,只处理 UIApplication.shared 逻辑,并将该pod添加到 MyApp 中。但那似乎很麻烦, 特别是因为我不知道如何在一个项目中部署依赖于相同源文件的2个CocoaPods。
如果这是唯一的解决方案,那么似乎将Git Submodules直接放入应用程序中更好,这样我就可以直接将其作为这些目标的一部分,那么 #if 应该就能正常工作了。问题在于,如果使用Git Submodules,将无法处理CocoaPod的依赖关系。因此,我真的必须以某种方式使用CocoaPods。
我希望有一个简单的解决方案,不像那些看起来很hacky。那么,有没有更好的方法来处理这个问题并修复错误,而不需要重写大量代码,并且不是超级hacky的解决方案?
在评论中,建议使用NSSelectorFromStringUIApplication.respondsUIApplication.perform。问题在于,如果苹果公司改变API,代码将会崩溃,即使是对于先前版本的应用程序,因为它被动态调用,没有API未来证明。虽然那个解决方案听起来很容易,但似乎是一个非常糟糕的决定。
下面这篇回答看起来非常有前途。不幸的是,在评论中概述了一些更改后,它仍然无法正常工作,主应用程序具有Core subspec以及AppExtension subspec。

可能的替代方法在这里:https://dev59.com/3V4b5IYBdhLWcg3wzUja#52257828,并且AFNetworking也存在类似的问题,可以使用post_install钩子进行解决:https://dev59.com/Rl4b5IYBdhLWcg3wiSPV#29335471 - jscs
@JoshCaswell 那个第二个链接看起来更有希望,但是没有起作用。第一个链接似乎非常hacky,但最终可能会成为一个不错的解决方案。使用第一个解决方案会失去编译器等所有安全性。 - Charlie Fish
你有检查过这个吗?https://dev59.com/YFgQ5IYBdhLWcg3w3Hp- - Sagar Chauhan
它正在使用的框架,你不能fork它并添加IF_EXTENTION吗? - MCMatan
@MCMatan 我写了 MyCustomSwiftPackage,它是一个自定义的私有 CocoaPod。当然名字已经改变了。但是在这个例子/问题中,我对所有源代码都有完全的控制。问题是,我无法弄清楚如何在使用 UIApplication.shared 的同时为应用程序和扩展使用一个干净的代码库。 - Charlie Fish
显示剩余9条评论
2个回答

2
假设你是 MyLibrary 的所有者: 最初的回答
Pod::Spec.new do |s|
  s.name             = "MyLibrary"
  # Omitting metadata stuff and deployment targets

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

你使用了不可用的API,因此代码根据名为MYLIBRARY_APP_EXTENSIONS的预处理器宏条件编译某些部分。我们声明了一个称为Core的子规范,其中包含所有代码,但标志关闭。如果用户未指定任何规范,则将该子规范设置为默认规范。然后,我们将声明另一个子规范,称为AppExtension,其中包括所有代码,但设置预处理器宏:

Original Answer翻译成"最初的回答"

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

然后在您的应用程序Podfile中,您将链接到主应用程序目标中的Core,以及在扩展中链接到AppExtension,如下所示:

在您的应用程序Podfile中,您将链接到主应用程序目标中的Core,以及在扩展中链接到AppExtension,如下所示:

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!


1
太好了。那么我就可以在我的代码库中使用 #if MYLIBRARY_APP_EXTENSIONS 来检查是否在扩展中了? - Charlie Fish
是的,没错 (; - MCMatan
1
我还没有测试这个。如果在奖励时间到期之前我没有机会测试,并且同时没有更好的答案,我将授予您奖励。一旦我真正开始测试它,我会接受并点赞。我认为它应该完美地工作。 - Charlie Fish
很高兴我能帮助到你 (: - MCMatan
我在我的应用程序扩展中遇到了一个奇怪的编译错误,错误信息是 No such module 'MyLibrary'。看起来这并没有影响到我的主应用程序目标。 - Charlie Fish
显示剩余6条评论

0

由于您只需访问UIApllication.shared才能获取topViewController。您不能将其作为框架依赖项,需要用户提供。

让我们声明提供程序,并使用precondition确保开发人员不会忘记设置此属性:

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!
        }
    }
}

然后在你的应用程序中,你可以这样做:

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

在扩展中,您可以始终返回 self。
通过任何视图获取 topViewController 的另一种方法:
extension UIView {
    func topViewController() -> UIViewController? {
        /// But I not sure that `window` is accessible in extension
        let root = window?.rootViewController
        ....
    }
}

我更喜欢基于 Pod 而不是客户端应用程序的解决方案。另外,rootViewController 与堆栈中的顶部视图控制器并不相同。 - Charlie Fish
@CharlieFish,你需要一些启动控制器来实现你的topViewController算法。我会展示如何获取这个起始点。 - ManWithBear
有道理。但似乎应该有一种方法可以在不需要客户端应用程序的情况下完成这个过程。它应该全部在Pod内完成。 - Charlie Fish
另一方面,@CharlieFish,你试图将只被一个特定客户使用而不被其他人使用的代码放入Pod中。这有意义吗?我会说这是项目特定的。所以这取决于你。 - ManWithBear
我不明白你的意思。另一个答案看起来非常有前途,因为它基本上都在 Pod 内部完成。但它却无法正常工作。 - Charlie Fish
@CharlieFish 我的意思是,如果 Pod 包含所有平台都使用的代码会更好。而只在一个平台上使用的代码应该要么提取到应用程序中,要么提取到另一个 Pod 中。 - ManWithBear

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接