swift-composable-architecture icon indicating copy to clipboard operation
swift-composable-architecture copied to clipboard

Unable to use TCA in extensions

Open IgorRosocha opened this issue 2 years ago • 5 comments

Description

We are currently taking leverage of Tuist dependencies to handle dependencies in our modular app. Until the release of version 0.41.0, everything worked fine, since then we're unable to update because we're getting errors in OpenURL.swift:

'open(_:options:completionHandler:)' is unavailable in application extensions for iOS
'shared' is unavailable in application extensions for iOS: Use view controller based solutions where appropriate instead.

Checklist

  • [x] I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • [X] If possible, I've reproduced the issue using the main branch of this package.
  • [X] This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

We should be able to use TCA in extensions / TCA should compile successfully.

Actual behavior

We are unable to use TCA in extensions / TCA doesn't compile successfully.

Steps to reproduce

  1. Integrate TCA using Tuist dependencies / use TCA in extension
  2. Compile the project

The Composable Architecture version information

0.45.0

Destination operating system

iOS 16

Xcode version information

14.0.1

Swift Compiler version information

swift-driver version: 1.62.8 Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50)
Target: arm64-apple-macosx12.0

IgorRosocha avatar Nov 16 '22 12:11 IgorRosocha

Hi @IgorRosocha, this was fixed in 0.42.0. Have you tried that? Also there has been many releases since then, so we recommend updating to the newest.

mbrandonw avatar Nov 16 '22 13:11 mbrandonw

Hi @mbrandonw, as mentioned in the issue, we're getting the error on the latest release - 0.45.0

IgorRosocha avatar Nov 16 '22 13:11 IgorRosocha

Oh sorry, I saw your first mention of 0.41 and thought that is what you were using. Are you able to share a sample project that reproduces the error so that we can figure out how to fix it?

mbrandonw avatar Nov 16 '22 13:11 mbrandonw

Sure, please take a look at minimal example here: https://github.com/IgorRosocha/TCA-Extension-Sample

To build the project:

  1. Install Tuist (curl -Ls https://install.tuist.io | bash)
  2. Run tuist fetch to fetch dependencies
  3. Run tuist generate to generate and open the project

Please let me know in case you run into some problems when generating and building the project.

IgorRosocha avatar Nov 16 '22 14:11 IgorRosocha

Hey @IgorRosocha! Would you mind taking my tuist-fix branch for a spin? https://github.com/tgrapperon/swift-composable-architecture/tree/tuist-fix I didn't install Tuist on my machine, but I guess you can point to a specific branch instead of a tagged release. I simply marked the APIs @available(iOSApplicationExtension, unavailable) which is the fix I used in the past to share code in dynamic frameworks between my app and its extensions. I can't say if this is the best solution when importing a SPM dependency via Tuist though (nor if it's a solution at all!).

tgrapperon avatar Nov 17 '22 07:11 tgrapperon

Hey @tgrapperon, sorry for such a late response. Fix introduced in your tuist-fix branch resolves our issue - the only question is if it's safe for you guys to mark openURL as unavailable for iOS application extensions, or maybe introduce some workaround for extensions.

IgorRosocha avatar Nov 28 '22 12:11 IgorRosocha

I've tried to implement a workaround, but without success. We don't have any installed UIViewController at hand to extract the extension context, so the best we can do is falling back to SwiftUI's openURL, but I didn't manage to make it build in an app extension context while preserving the current UIKit branch for regular iOS apps.

tgrapperon avatar Nov 28 '22 14:11 tgrapperon

Currently we use TCA with CocoaPod integration and also faced with same issue. I can suggest only ugly solution:

extension UIApplication {
    static func openIfAvailable(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], completionHandler completion: ((Bool) -> Void)? = nil) {
        let sharedApplication = NSSelectorFromString("sharedApplication")
        let openURL = NSSelectorFromString("openURL:options:completionHandler:")

        guard
            responds(to: sharedApplication),
            let application = perform(sharedApplication).takeRetainedValue() as? UIApplication,
            application.responds(to: openURL) else {
            // Or use SwiftUI implementation?
            completion?(false)
            return
        }

        typealias Callback = @convention(c) (NSObject, Selector, URL, NSDictionary, @escaping (Bool) -> Void) -> Void
        let callback = unsafeBitCast(application.method(for: openURL), to: Callback.self)
        callback(application, openURL, url, options as NSDictionary, { completion?($0) })
    }
}

And what will happen in runtime of an App Extension with integrated TCA with SPM? Seems to SPM ignored APPLICATION_EXTENSION_API_ONLY flag in build settings and allow to perform UIApplication.shared and other methods which mark as @available(iOSApplicationExtension, unavailable).

rock88 avatar Dec 01 '22 15:12 rock88

When used with SPM, the current implementation works, and without producing any error. Besides NSSelectorFromString, there are two ways to fix this issue AFAIK:

  • Annotate this dependency with @available(iOSApplicationExtension, unavailable), which makes code harder to reuse
  • Base the implementation on SwiftUI's EnvironmentValues.openURL, which raises the availability to iOS 14+ generation.

It is not clear yet what is the best solution, but I'm confident it will be addressed in one way or another.

tgrapperon avatar Dec 01 '22 16:12 tgrapperon

Fixed by #1714.

stephencelis avatar Dec 06 '22 17:12 stephencelis