Dynamic Swift Framework: Fatal error: unable to find bundle - swift

I have a framework that imports several Swift Packages.
One such package uses Bundle.myModule for resources. Here is the code use for .myModule
extension Foundation.Bundle {
static var myModule: Bundle = {
let bundleNameIOS = "xxxx"
let candidates = [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: CurrentBundleFinder.self).resourceURL,
// Bundle should be present here for package UI Tests.
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent(),
// For command-line tools. */
Bundle.main.bundleURL,
// Bundle should be present here when running previews from a different package (this is the path to "…/Debug-iphonesimulator/").
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent(),
Bundle(for: CurrentBundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),
]
for candidate in candidates {
let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle")
if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) {
return bundle
}
if let bundle = Bundle(path: "\(Bundle.main.bundlePath)/\(bundleNameIOS).bundle") {
return bundle
}
}
fatalError("unable to find bundle")
}()
}
Then we I build the framework and embed it inside another project I get the error
Fatal error: unable to find bundle
But the Package that this is failing in is inside the XCFramework
the folder path "App/Frameworks/myFramework" does not contain any bundles
I have tried all of the follow:
Swift Package: Fatal error: unable to find bundle named
https://developer.apple.com/forums/thread/650158
Swift Package Manager - Type 'Bundle' has no member “module” error
https://forums.swift.org/t/unable-to-find-bundle-in-package-target-tests-when-package-depends-on-another-package-containing-resources-accessed-via-bundle-module/43974/6
Please any help would be greatly appreciated. Thank you

Related

Cannot access Swift Package xcassets

I'm trying to use a color set from a xcassets folder that is inside a Swift Package (src). It doesn't seem to be working. I tested this out by writing a simple view that attempts to make use of the color:
Text("Hello")
.foregroundColor(Color("brandPrimary")
I got an empty view from this code; the text can't find the color.
I've done significant research around the web - reviewing WWDC videos such as Swift packages: Resources and localization, and they seem to suggest that xcassets folder are automatically included as resources. It doesn't work for me.
I tried to add process("Resources/Colors.xcassets") inside my package manifest, but that didn't help either.
From the Color documentation:
init(_ name: String, bundle: Bundle? = nil)
…
bundle
The bundle in which to search for the color resource. If you don’t indicate a bundle, the initializer looks in your app’s main bundle by default.
Your GoodPackage library's assets are not the app's main bundle, so you need to tell the Color initializer which bundle to search.
The Swift package manager's build process automatically creates a Bundle for each target/module that contains assets. Within the module, you can access that generated Bundle using the expression Bundle.module. SwiftPM actually writes Swift code to a file named resource_bundle_accessor.swift in your DerivedData to make this work. The generated source code looks like this:
import class Foundation.Bundle
import class Foundation.ProcessInfo
import struct Foundation.URL
private class BundleFinder {}
extension Foundation.Bundle {
/// Returns the resource bundle associated with the current Swift module.
static let module: Bundle = {
let bundleName = "YourPackageName_YourTargetName"
let overrides: [URL]
#if DEBUG
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] {
overrides = [URL(fileURLWithPath: override)]
} else {
overrides = []
}
#else
overrides = []
#endif
let candidates = overrides + [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: BundleFinder.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
]
for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
fatalError("unable to find bundle named YourPackageName_YourTargetName")
}()
}
As you can see, that static let module property has (default) internal access, so you can only use it from source code within that module. By default, there is no way to access the module's Bundle from outside the module.
One solution is to add a public accessor for the module Bundle in the GoodPackage module. For example, add this to a source file in the GoodPackage module:
import Foundation
extension Bundle {
public var GoodPackage: Bundle { Bundle.module }
}
Then, in your app:
import GoodPackage
...
Text("Hello")
.foregroundColor(Color("brandPrimary", bundle: .GoodPackage))

Spawning a process in an app built with UIKit for macOS (Catalyst)

I'm building an application that shares most of the code between macOS and iOS versions (targeting macOS 11 and iOS 14). UIKit for Mac seems like a natural choice to help with this. Unfortunately, one of the libraries uses the Process type under the hood. Building it produces "Cannot find type Process in scope" error when a dependency on it is added and when targeting macOS. I'm fine with excluding this library for iOS, but I still need to link with it on macOS while keeping the ability to use UIKit on all platforms.
I've selected this library to be linked only for macOS in Xcode, but this has no effect and the same build error persists. Also, I'm getting this error without adding a single import SwiftLSPClient statement in the app, so I don't think conditional imports would help in this case.
What would be the best way to resolve this issue within the constraints listed above?
I created a LSPCatalyst class in my Mac Catalyst app to replace the MacOS LanguageServerProcessHost. To make that work, I replaced the process property with a processProxy that accesses the process instance in a MacOS bundle using the FoundationApp protocol as explained below.
Following #Adam's suggestion, I created a MacOS bundle to proxy for the process instance. You follow the same idea as he pointed to for AppKit access from Catalyst apps, but you just need Foundation to get access to Process. I called the bundle FoundationGlue and put everything in a FoundationGlue folder in my Xcode project. The bundle needs an Info.plist that identifies the principal class as "FoundationGlue.MacApp", and the MacApp.swift looks like:
import Foundation
class MacApp: NSObject, FoundationApp {
var process: Process!
var terminationObserver: NSObjectProtocol!
func initProcess(_ launchPath: String!, _ arguments: [String]?, _ environment: [String : String]?) {
process = Process()
process.launchPath = launchPath
process.arguments = arguments
process.environment = environment
}
func setTerminationCompletion(_ completion: (()->Void)!) {
let terminationCompletion = {
NotificationCenter.default.removeObserver(self.terminationObserver!)
completion?()
}
terminationObserver =
NotificationCenter.default.addObserver(
forName: Process.didTerminateNotification,
object: process,
queue: nil) { notification -> Void in
terminationCompletion()
}
}
func setupProcessPipes(_ stdin: Pipe!, _ stdout: Pipe!, _ stderr: Pipe!) {
process.standardInput = stdin
process.standardOutput = stdout
process.standardError = stderr
}
func launchProcess() {
process.launch()
print("Launched process \(process.processIdentifier)")
}
func terminateProcess() {
process.terminate()
}
func isRunningProcess() -> Bool {
return process.isRunning
}
}
The corresponding header I called FoundationApp.h looks like:
#import <Foundation/Foundation.h>
#protocol FoundationApp <NSObject>
typedef void (^terminationCompletion) ();
- (void)initProcess: (NSString *) launchPath :(NSArray<NSString *> *) arguments :(NSDictionary<NSString *, NSString *> *) environment;
- (void)setTerminationCompletion: (terminationCompletion) completion;
- (void)setupProcessPipes: (NSPipe *) stdin :(NSPipe *) stdout :(NSPipe *) stderr;
- (void)launchProcess;
- (void)terminateProcess;
- (bool)isRunningProcess;
#end
And the FoundationAppGlue-Bridging-Header.h just contains:
#import "FoundationApp.h"
Once you have the bundle built for MacOS, add it as a framework to your Mac Catalyst project. I created a Catalyst.swift in that project for access to the FoundationGlue bundle functionality::
import Foundation
#available(macCatalyst 13, *)
struct Catalyst {
/// Catalyst.foundation gives access to the Foundation functionality identified in FoundationApp.h and implemented in FoundationGlue/MacApp.swift
static var foundation: FoundationApp! {
let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("FoundationGlue.bundle")
let bundle = Bundle(path: url!.path)!
bundle.load()
let cls = bundle.principalClass as! NSObject.Type
return cls.init() as? FoundationApp
}
}
Then, you use it from your app like:
let foundationApp = Catalyst.foundation!
foundationApp.initProcess("/bin/sh", ["-c", "echo 1\nsleep 1\necho 2\nsleep 1\necho 3\nsleep 1\necho 4\nsleep 1\nexit\n"], nil)
foundationApp.setTerminationCompletion({print("terminated")})
foundationApp.launchProcess()
This is a messy solution but I know it works: Add a “Mac bundle” to your Catalyst app and import the MacOS-only framework with that.
Here’s a guide to creating and loading a Mac bundle: https://medium.com/better-programming/how-to-access-the-appkit-api-from-mac-catalyst-apps-2184527020b5
Once you have the bundle, you can add Mac-only libraries and frameworks to it. You’ll have to bridge data and method calls between the bundle and your iOS app, but it’s manageable.

Why can't my app find Firebase Functions?

I can't figure out why I keep getting the Swift warning: Use of unresolved identifier 'Functions on this line of my code: let functions = Functions.functions()
My imports for the viewController includes import Firebase and it works fine when I declare let db = Firestore.firestore() right above the line let functions = Functions.functions()
My podfile includes pod 'Firebase/Functions' and I've installed the pod.
I'm calling functions later using the following code and when I type "functions" it recommends adding .httpsCallable which leads me to believe that it actually does recognize the object "functions":
func getData(){
functions.httpsCallable("helloWorld").call(userData) { (result, error) in
if let error = error {
print(error)
}
if let data = result?.data {
print(data)
}
}
}
Figured it out. Importing Firebase is not enough, one must also import FirebaseFunctions (despite what Swift thinks, see screenshot below).

NSClassFromString from dependencies

I'm having this problem getting the reflection to work from external frameworks as dependencies.
So let say I had two projects, one is a sample app project
SampleApp.xcodeproj
#obj protocol Extension : NSObjectProtocol {
//..Some definitions
}
And another with the framework project that the sample app will be depended on and is a submodule of the sample app project.
Framework.xcodeproj
class SampleClass : NSObject, Extension {
//some definitions
}
In this framework, Extension.swift file, where the Extension protocol is defined, is included in the build as a reference
Now here's the problem on this code
if let customExtension = NSClassFromString("Framework.SampleClass") as? NSObject.Type {
let extensionInstance = customExtension.init()
if extensionInstance is Extension { //The problem here, it returned false at this point
print("It is an Extension")
} else {
print("It is not an Extension")
}
}
I noticed that the Extension protocol defined in the Framework project was defined as Framework.Extension instead of SampleApp.Extension. How would I make it so the SampleClass class is defined as SampleApp.Extension instead of Framework.Extension?

How to get bundle for a struct?

In Swift, you can call
let bundle = NSBundle(forClass: self.dynamicType)
in any class and get the current bundle. If you NSBundle.mainBundle() this will fail getting correct bundle when for example running unit tests.
So how can you get the current bundle for a Swift struct?
The best solution here depends on what you need a bundle for.
Is it to look up resources that exist only in a specific app, framework, or extension bundle that's known to be loaded when the code you're writing runs? In that case you might want to use init(identifier:) instead of dynamically looking up the bundle that defines a certain type.
Beware of "follows the type" bundle lookups. For example, if a framework class Foo uses NSBundle(forClass: self.dynamicType) to load a resource, a subclass of Foo defined by the app loading that framework will end up looking in the app bundle instead of the framework bundle.
If you do need a "follows the type" bundle lookup for a struct (or enum), one workaround that might prove helpful is to define a class as a subtype:
struct Foo {
class Bar {}
static var fooBundle: NSBundle { return NSBundle(forClass: Foo.Bar.self) }
}
Note there's nothing dynamic here, because nothing needs to be — every Foo comes from the same type definition (because structs can't inherit), so its static type matches its dynamic type.
(Admittedly, an NSBundle(forType:) that could handle structs, enums, and protocols might make a nice feature request. Though I imagine it could be tricky to make it handle extensions and everything...)
extension Bundle {
static var current: Bundle {
class __ { }
return Bundle(for: __.self)
}
}
Updated for Swift 3.0+:
struct Foo {
class Bar {}
static var fooBundle: Bundle { return Bundle(for: Foo.Bar.self) }
}
Swift 5
struct Foo {
class Bar {}
static var fooBundle: Bundle { return Bundle(for: Foo.Bar.self) }
}
For Swift Packages, we get the Bundle where the Swift struct is declared anywhere in the same module (target) via:
Bundle.module
However, we need to import a resource in the Package to get this autogenerated.
Bundle.main also works for other projects if you don't have extension targets.
Swift 4+
You can do let bundle = InternalConstants.bundle if you add this struct to your project. A very elegant solution in my opinion.
internal struct InternalConstants {
private class EmptyClass {}
static let bundle = Bundle(for: InternalConstants.EmptyClass.self)
}
Another potential solution (less elegant):
internal struct InternalConstants {
internal static let bundle = Bundle(identifier: "com.hello.world")!
}