Localizable.strings from framework is not getting inside framework - ios

I have a project with some basic functionalities which I can reuse and I am trying to use that as a framework in another project. So my podspec includes strings, swift, assets files and all got added in to the project but the strings keys are not taking the value from localizables.strings. Basically it is not getting from the bundle path. Here is the code where I am calling the strings file inside the framework. "path" is getting as nil. It is working fine in the base project
let bundle: Bundle = .main
if let currentLanguage = languageManager.getCurrentLanguage() {
if let path = bundle.path(forResource: currentLanguage.rawValue, ofType: "lproj"),
let bundle = Bundle(path: path) {
return bundle.localizedString(forKey: self, value: nil, table: nil)
}
}

I fixed the issue.. In podspec, it was
spec.resource_bundle = { 'AppName' => ['AppName/**/*.{strings}'] }
I changed it to
spec.resources = "AppName/**/*.{xcassets}", "AppName/**/*.{Strings}"

Related

How to get the random file path for iOS TestRunner or TestBundle in KMM?

For UI test in Xcode I could read a file under UITest bundle like this.
func testReadFile() throws {
// Get Bundle for UI test target
let testBundle = Bundle(for: XCTestBase.self)
// Find the file belongs to current test runner.
if let filePath = testBundle.url(forResource: "good", withExtension: "txt") {
print(filePath)
}
}
And with PO, I know it will use the iosAppUITests-Runner instance as the path for file good.txt. So that is a random path only for runtime use.
(lldb) po testBundle.url(forResource: "good", withExtension: "txt")
▿ Optional<URL>
▿ some : file:///Users/haibozhou/Library/Developer/CoreSimulator/Devices/EFF818A9-6670-4635-9618-DEBFB2C15156/data/Containers/Bundle/Application/DA9E0623-D0DF-414C-A831-5654BBB64AF8/iosAppUITests-Runner.app/PlugIns/iosAppUITests.xctest/good.txt
- _url : file:///Users/haibozhou/Library/Developer/CoreSimulator/Devices/EFF818A9-6670-4635-9618-DEBFB2C15156/data/Containers/Bundle/Application/DA9E0623-D0DF-414C-A831-5654BBB64AF8/iosAppUITests-Runner.app/PlugIns/iosAppUITests.xctest/good.txt
In KMM, I want to implement the same for iOS. However I got a problem. It seems the test class(XCTestBase) is not resolved for KMM project. Because it is not exposed to KMM project I guess.
val testBundle = NSBundle.bundleForClass(XCTestBase)
After I try things like this with a relative path, still without luck. Thus, what is the correct way to find the file path from Kotlin iOS code for iOS test runner?
actual fun readFile(path: String): String {
val string = NSString.stringWithContentsOfFile(
"../iosAppUITests-Runner.app/PlugIns/iosAppUITests.xctest/good.txt",
NSUTF8StringEncoding, null) ?: return "File not found!"
// ignored code...
return string
}
Eventually, I figure out how to do it today.
Basically, first get all Bundles then filter out the test bundle, in my case it named iosAppUITests.xctest, replace it with your test target name in your project. Now we have the test bundle finally! We could get the dictionary from this testBundle and key-values after. And because KMM is interops with iOS by Objective-C framework but not Swift framework, we need do it with Objective-C format.
Well, it is a workaround for now.
actual fun getEnv(key: String): String {
// Get all bundles
val allBundles = NSBundle.allBundles
val bundles = allBundles.filterIsInstance<NSBundle>()
.apply { if (size != allBundles.size) return "null" }
// Filter out bundle for xctest, which is the current test runner test bundle.
val testBundle: NSBundle = bundles.first {
it.isLoaded() &&
it.bundleURL.lastPathComponent == "iosAppUITests.xctest"
}
// Get the dict, in KMM it is Map<*,*>
val path = testBundle.pathForResource("fileName", "plist")
val dict = path?.let { NSDictionary.dictionaryWithContentsOfFile(it) }
println("~ dict: $dict")
return dict?.get(key) as String
}

Use resource bundles in CocoaPods

I am making a pod (MySDK) and would like to load the assets from the separate resource bundles CocoaPods generates.
However, I can not get it to work.
Here is how I tried to load the storyboard:
let storyBoard = UIStoryboard(name: "SDK", bundle: Bundle(identifier:"org.cocoapods.SchedJoulesSDK"))
This gives the error:
'Could not find a storyboard named 'SDK' in bundle
The bundle is added in Xcode:
And my podspec looks like this:
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
}
Any ideas?
If you use resource or resources in a CocoaPods PodSpec file, you tell Cocoapods that these are the resource files your library will load during runtime.
If your library is built as a dynamic framework, these files are just copied to the resource folder path of that framework and everything will be fine. Yet if your library is built as a static library, these are copied to the resource folder of the main application bundle (.app) and this can be a problem as this main application may already have a resource of that name or another Pod may have a resource of that name, in that case these files will overwrite each other. And whether a Pod is built as dynamic framework or as a static library is not specified by the PodSpec but in the Podfile by the application integrating your Pod.
Thus for Pods with resources, it is highly recommended to use resource_bundles instead!
In your case, the lines
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}'] }
tell CocoaPods to create a resource bundle named MySDK (MySDK.bundle) and place all files matching the pattern into that resource bundle. If your Pod is built as a framework, this bundle is located in the resources folder of your framework bundle; if it is built as a static library, the bundle is copied to the resources folder of the main application bundle, which should be safe if you name your bundle the same way as your Pod (you should not name it "MySDK", rather "SchedJoulesSDK").
This bundle will have the same identifier as your Pod, however when dynamic frameworks are built, your framework bundle will have that identifier as well and then it's undefined behavior which bundle is being loaded when you load it by identifier (and currently the outer bundle always wins in my tests).
Correct code would look like this (not tested, though):
// Get the bundle containing the binary with the current class.
// If frameworks are used, this is the frameworks bundle (.framework),
// if static libraries are used, this is the main app bundle (.app).
let myBundle = Bundle(for: Self.self)
// Get the URL to the resource bundle within the bundle
// of the current class.
guard let resourceBundleURL = myBundle.url(
forResource: "MySDK", withExtension: "bundle")
else { fatalError("MySDK.bundle not found!") }
// Create a bundle object for the bundle found at that URL.
guard let resourceBundle = Bundle(url: resourceBundleURL)
else { fatalError("Cannot access MySDK.bundle!") }
// Load your resources from this bundle.
let storyBoard = UIStoryboard(name: "SDK", bundle: resourceBundle)
As resourceBundle cannot change at runtime, it is safe to create it only once (e.g. on app start or when your framework is initialized) and store it into a global variable (or global class property), so you have it always around when needed (a bundle object also hardly uses any RAM memory, as it only encapsulates meta data):
final class SchedJoulesSDK {
static let resourceBundle: Bundle = {
let myBundle = Bundle(for: SchedJoulesSDK.self)
guard let resourceBundleURL = myBundle.url(
forResource: "MySDK", withExtension: "bundle")
else { fatalError("MySDK.bundle not found!") }
guard let resourceBundle = Bundle(url: resourceBundleURL)
else { fatalError("Cannot access MySDK.bundle!") }
return resourceBundle
}()
}
The property is initialized lazy (that's default for static let properties, no need for the lazy keyword) and the system ensures that this happen only once, as a let property must not changed once initialized. Note that you cannot use Self.self in that context, you need to use the actual class name.
In your code you can now just use that bundle wherever needed:
let storyBoard = UIStoryboard(name: "SDK",
bundle: SchedJoulesSDK.resourceBundle)
You can use like...
s.resource = "icon.png" //for single file
or
s.resources = "Resources/*.png" //for png file
or
s.resources = "Resources/**/*.{png,storyboard}" //for storyboard and png files
or
s.resource_bundles = {
"<ResourceBundleName>" => ["path/to/resources/*/**"]
}
This is what I ended up with...
import Foundation
public extension Bundle {
public static func resourceBundle(for frameworkClass: AnyClass) -> Bundle {
guard let moduleName = String(reflecting: frameworkClass).components(separatedBy: ".").first else {
fatalError("Couldn't determine module name from class \(frameworkClass)")
}
let frameworkBundle = Bundle(for: frameworkClass)
guard let resourceBundleURL = frameworkBundle.url(forResource: moduleName, withExtension: "bundle"),
let resourceBundle = Bundle(url: resourceBundleURL) else {
fatalError("\(moduleName).bundle not found in \(frameworkBundle)")
}
return resourceBundle
}
}
Example usage...
class MyViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: Bundle.resourceBundle(for: Self.self))
}
}
or
tableView.register(UINib(nibName: Self.loginStatusCellID, bundle: Bundle.resourceBundle(for: Self.self)), forCellReuseIdentifier: Self.loginStatusCellID)
I have the same issue, if I only use the bundle line:
s.resource_bundles = {
'MySDK' => ['SDK/*/*.{xib,storyboard,xcassets}']
}
I get a runtime crash when I reference my storyboard. However, if I explicitly add each storyboard as a resource like so:
s.resources = ["Resources/MyStoryboard.storyboard", "Resources/MyStoryboard2.storyboard"]
Everything works fine. I don't think we should have to explicitly add each storyboard as a resource, but I haven't been able to make it work any other way.
One thing you could try is changing you resource bundle reference to recursively search in your folders with the ** nomenclature like this:
s.resource_bundles = {
'MySDK' => ['SDK/**/*.{xib,storyboard,xcassets}']
}

Get all URLs for resources in sub-directory in Swift

I am attempting to create an array of URLs for all of the resources in a sub-directory in my iOS app. I can not seem to get to the correct path, I want to be able to retrieve the URLs even if I do not know the names (i.e. I don't want to hard code the file names into the code).
Below is a screen shot of the hierarchy, I am attempting to get all the files in the 'test' folder:
Any help is greatly appreciated, I have attempted to use file manager and bundle main path but to no joy so far.
This is the only code I have currently:
let fileManager = FileManager.default
let path = Bundle.main.urls(forResourcesWithExtension: "pdf", subdirectory: "Files/test")
print(path)
I have also tried this code but this prints all resources, I can't seem to specify a sub-directory:
let fm = FileManager.default
let path = Bundle.main.resourcePath!
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
print("Found \(item)")
}
} catch {
// failed to read directory – bad permissions, perhaps?
}
Based on an answer from #vadian , The folders were changed from virtual groups to real folders. Using the following code I was able to get a list of resources:
let fileManager = FileManager.default
let path = Bundle.main.resourcePath
let enumerator:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: "\(path!)/Files/test")!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("pdf") || element.hasSuffix("jpg") { // checks the extension
print(element)
}
}
Consider that the yellow folders are virtual groups, not real folders (although Xcode creates real folders in the project directory). All files in the yellow folders are moved into the Resources directory in the bundle when the app is built.
Real folders in the bundle are in the project navigator.
You can follow the following steps to get them:
Create a new folder inside your project folder with the extension is .bundle (for example: Images.bundle).
Copy resource files into that new folder.
Drag that new folder into the project that opening in Xcode.
Retrieve the URLs by using the following code snippet:
let urls = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: "Images.bundle")
You can also view the guide video here: https://youtu.be/SpMaZp0ReEo
I came across a similar issue today. I needed to retrieve the URL of a resource file in a bundle ignoring its path.
I wrote the following:
public extension Bundle {
/// Returns the file URL for the resource identified by the specified name, searching all bundle resources.
/// - Parameter resource: The name of the resource file, including the extension.
/// - Returns: The file URL for the resource file or nil if the file could not be located.
func recurseUrl(forResource resource: String) -> URL? {
let contents = FileManager.default.allContentsOfDirectory(atPath: self.bundlePath)
for content in contents {
let fileNameWithPath = NSString(string: content)
if let fileName = fileNameWithPath.pathComponents.last {
if resource == fileName {
return URL(fileURLWithPath: content)
}
}
}
return nil
}
Based on this:
public extension FileManager {
/// Performs a deep search of the specified directory and returns the paths of any contained items.
/// - Parameter path: The path to the directory whose contents you want to enumerate.
/// - Returns: An array of String objects, each of which identifies a file or symbolic link contained in path. Returns an empty array if the directory does not exists or has no contents.
func allContentsOfDirectory(atPath path: String) -> [String] {
var paths = [String]()
do {
let url = URL(fileURLWithPath: path)
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
for content in contents {
let contentUrl = url.appendingPathComponent(content)
if contentUrl.hasDirectoryPath {
paths.append(contentsOf: allContentsOfDirectory(atPath: contentUrl.path))
}
else {
paths.append(contentUrl.path)
}
}
}
catch {}
return paths
}
}
Which achieves the goal of retrieving the URL for the first match of a given resource filename in a bundle's resources, all directories wide.
I tend to think that Swift's func url(forResource name: String?, withExtension ext: String?) -> URL? should behave this way in the first place.

Read local JSON file in XCUITest

I am writing UI tests for iOS application. I want to read data from a local json file.
I am using the following code to get the path for my json file:
func testExample() {
readJSON()
}
func readJSON(){
let bundle = Bundle(for:myTestClass.self)
if let path = bundle.url(forResource: "myurl", withExtension: "json"){
print("Got the path")
print(path)
}
else{
print("Invalid filename/path")
}
I have tried using the solution for the following stackoverflow question :
Reading in a JSON File Using Swift. Didn't work!
Also, I had a look at the following Apple documentation:
https://developer.apple.com/swift/blog/?id=37
Any help would be greatly appreciated!
First of all, you need to check the .json file is in the same target with the test file. If the file is in the main target, you have to use Bundle.main. However if it is in the same target with your test, use the below code.
let t = type(of: self)
let bundle = Bundle(for: t.self)
let path = bundle.path(forResource: "myurl", ofType: "json")
That's not how you read local json files from Bundle in iOS. It's done in the following way:
// "myUrl" is assumed to be your json file name
if let pathStr: String = Bundle.main.path(forResource: "myurl", ofType: ".json") {
print("json path: \(pathStr)")
}
I just found resources added to XCUITest targets will be placed under Bundle.main.resourcePath + "/PlugIns/<TARGET_NAME>.xctest/". However, I'm not sure if there were better ways to access them rather than hard-coding the sub-directory path.

How to load resource in cocoapods resource_bundle

I have struggled a lot to how to load resource in cocoapods resource_bundle.
The following is what i put in the .podspecs file.
s.source_files = 'XDCoreLib/Pod/Classes/**/*'
s.resource_bundles = {
'XDCoreLib' => ['XDCoreLib/Pod/Resources/**/*.{png,storyboard}']
}
This is what I am trying to do from the main project.
let bundle = NSBundle(forClass: XDWebViewController.self)
let image = UIImage(named: "ic_arrow_back", inBundle: bundle, compatibleWithTraitCollection: nil)
print(image)
I did see the picture in the XDCoreLib.bundle, but it return a nil.
I struggled with a similar issue for a while. The resource bundle for a pod is actually a separate bundle from where your code lives. Try the following:
Swift 5
let frameworkBundle = Bundle(for: XDWebViewController.self)
let bundleURL = frameworkBundle.resourceURL?.appendingPathComponent("XDCoreLib.bundle")
let resourceBundle = Bundle(url: bundleURL!)
let image = UIImage(named: "ic_arrow_back", in: resourceBundle, compatibleWith: nil)
print(image)
-- ORIGINAL ANSWER --
let frameworkBundle = NSBundle(forClass: XDWebViewController.self)
let bundleURL = frameworkBundle.resourceURL?.URLByAppendingPathComponent("XDCoreLib.bundle")
let resourceBundle = NSBundle(URL: bundleURL!)
let image = UIImage(named: "ic_arrow_back", inBundle: resourceBundle, compatibleWithTraitCollection: nil)
print(image)
I don't think any of the other answers have described the relationship to the Podspec. The bundle URL uses the name of the pod and then .bundle to find the resource bundle. This approach works with asset catalogs for accessing pod images both inside and outside of the pod.
Podspec
Pod::Spec.new do |s|
s.name = 'PodName'
s.resource_bundles = {
'ResourceBundleName' => ['path/to/resources/*/**']
}
end
Objective-C
// grab bundle using `Class` in pod source (`self`, in this case)
NSBundle *bundle = [NSBundle bundleForClass:self.classForCoder];
NSURL *bundleURL = [[bundle resourceURL] URLByAppendingPathComponent:#"PodName.bundle"];
NSBundle *resourceBundle = [NSBundle bundleWithURL:bundleURL];
UIImage *podImage = [UIImage imageNamed:#"image_name" inBundle:resourceBundle compatibleWithTraitCollection:nil];
Swift
See this answer.
This will work
In .podspec add s.resources in addition to s.resource_bundles (pod install afterwards)
s.resources = 'XDCoreLib/Pod/Resources/**/*.{png,storyboard}'
Then in your code, its as easy as:
let image = UIImage(named: "image_name.png", in: Bundle(for: type(of: self)), compatibleWith: nil)
Note: You may need to run pod install after updating the .podspec to have the changes to your Xcode proj
You can just check the ID of the Bundle by selecting the Pods project, selecting the desired target, and making sure you're on the General tab.
Then in code you can load say an image in that Pod like so:
backgroundImageView.image = UIImage(named: "business_fellas", in: Bundle(identifier: "org.cocoapods.StandardLibrary3-0"), compatibleWith: nil)
Can create an extension to access the framework bundle easier in your framework source code
extension Bundle {
static func getResourcesBundle() -> Bundle? {
let bundle = Bundle(for {your class}.self)
guard let resourcesBundleUrl = bundle.resourceURL?.appendingPathComponent({bundle name}) else {
return nil
}
return Bundle(url: resourcesBundleUrl)
}
}
Just for the record: I wanted to open a NIB from a Category stored in a CocoaPods library. Of course [self classForCoder] gave UIKit back (because of the Category), so the above methods didn't work.
I resolved the issue using some hardcoded paths:
NSURL *bundleURL = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:#"Frameworks/MyCocoaPodLibraryName.framework"];
NSBundle *podBundle = [NSBundle bundleWithURL:bundleURL];
CustomView *customView = [[podBundle loadNibNamed:#"CustomView" owner:self options:nil] firstObject];
It seems like for some reason **/*.{extensions} pattern is required for this to work I ended up creating my podspec like this
s.resource_bundle = { '<BundleName>' => 'Pod/Resources/**/*.storyboard' }
s.resource = 'Pod/Resources/**/*.storyboard'
even though my actual path is Pod/Resources/.storyboard*
And then to find my bundle I used
#Jhelzer's answer although any way to get bundle e.g. by class or URL or bundleID will work after above setup.
You can also have a look into this answer for more refrences.
Interesting thing is when you need to do it in the source files that are also in the cocoapod library
I faced it when used a xib and plist files in my pod.
That was solved in the next way. Example of loading a xib file:
let bundle = Bundle(for: self.classForCoder)
let viewController = CocoapodViewController(nibName: "CocoapodViewController", bundle: bundle)
import class Foundation.Bundle
private class BundleFinder {}
extension Foundation.Bundle {
/// Returns the resource bundle associated with the current Swift module.
static var module: Bundle = {
let bundleName = "ID3TagEditor_ID3TagEditorTests"
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: 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 ID3TagEditor_ID3TagEditorTests")
}()
}
From: https://www.fabrizioduroni.it/2020/10/19/swift-package-manager-resources/
For me the following worked because I'm using a static pod.
s.resource_bundle = '<BundleName>' => 'Path to resources'
s.resource = 'Path to resources'

Resources