Is it possible to mock an applications NSBundle to return predictable results during TDD?
For example:
I want to test that my application handles when a file is not saved to the NSBundle:
//Method to test
func getProfileImage() -> UIImage {
if let profileImagePath = getProfilePhotoPath() {
UIImage(contentsOfFile: profileImagePath)
}
return UIImage(named: "defaultProfileImage")
}
private func getProfilePhotoPath() -> String? {
return NSBundle.mainBundle().pathForResource("profileImage", ofType: "png")
}
Is it possible to mock the NSBundle.mainBundle() to return false for a pathForResource?
As it stands, NSBundle.mainBundle() is a hard-coded dependency. What we'd like is the ability to specify any bundle, perhaps with mainBundle as the default. The answer is Dependency Injection. Let's use the preferred form of Constructor Injection, and take advantage of Swift's default parameters:
class ProfileImageGetter {
private var bundle: NSBundle
init(bundle: NSBundle = NSBundle.mainBundle()) {
self.bundle = bundle
}
func getProfileImage() -> UIImage {
if let profileImagePath = getProfilePhotoPath() {
return UIImage(contentsOfFile: profileImagePath)!
}
return UIImage(named: "defaultProfileImage")!
}
private func getProfilePhotoPath() -> String? {
return bundle.pathForResource("profileImage", ofType: "png")
}
}
Now a test can instantiate a ProfileImageGetter and specify any bundle it likes. This could be the test bundle, or a fake.
Specifying the test bundle would allow you to have a situation where profileImage.png doesn't exist.
Specifying a fake would let you stub the result of calling pathForResource().
Related
Localization is working fine in my application. I want localization the permission dialog text messages. It is working fine with the device language changes but I want to change message according to my application language.
I have tried the following code
import UIKit
class LocalizeHelper: NSObject {
private var myBundle: Bundle? = nil
static let shared: LocalizeHelper = {
let instance = LocalizeHelper()
return instance
}()
override init() {
super.init()
// use systems main bundle as default bundle
myBundle = Bundle.main
}
func localizedString(forKey key: String) -> String {
return myBundle!.localizedString(forKey: key, value: "", table: nil)
}
// Converted with Swiftify v1.0.6331 - https://objectivec2swift.com/
func setLanguage(_ lang: String) {
// path to this languages bundle
let path: String? = Bundle.main.path(forResource: lang, ofType: "lproj")
if path == nil {
// there is no bundle for that language
// use main bundle instead
myBundle = Bundle.main
}
else {
// use this bundle as my bundle from now on:
myBundle = Bundle(path: path!)
// to be absolutely shure (this is probably unnecessary):
if myBundle == nil {
myBundle = Bundle.main
}
}
}
func getLanguage() -> String {
print("\(String(describing: myBundle?.bundlePath.last))")
//return myBundle!.bundlePath.last >> Error
return myBundle!.bundlePath.lastCharacter!
}
}
extension String {
public var lastCharacter: String? {
guard let aLast = self.last else {
return nil
}
return String(aLast)
}
}
I have surfed in StackOverflow but didn't find any solution. Any help shell we appreciated.
Your best bet is to save the application's language in the user defaults as a string for example and then when you take it out of user defaults parse it as a custom app language enum. If you don't have any app language save you can always fall through to the device language but prefer or override it with the app language in user defaults not being nil. Also preferably you should wrap your user defaults interactions with a user defaults manager that you access in your view controller.
So the suggested steps are:
create userdefualts manager
define language enum
implement setLanguage func language parameter
implement getLanguage func language output with device language or
english as guard return
I have a cocoapod library which contains assets in 2 formats:
a .storyboard
XCode asset catalog .xcassets (with images)
my podspec file contains the definition for the resource bundle:
s.resource_bundle = {'SparkSetup' => ['Resources/**/*.{xcassets,storyboard}']}
and I have a separate target in the pod project to create a resource bundle by using those files + a plist file for that bundle.
thing is that when I use the pod in an app project - I can see the storyboard/xcassets files being in the pod target and I can access and run the storyboard easily but the images referenced in the storyboard (to the .xcassets file) are not found in run-time (but displayed correctly in IB).
Error I get is:
Could not load the "spinner" image referenced from a nib in the bundle with identifier "(null)"
I do see a bundle file in the products directory.
To instanciate VCs in the storyboard I use:
+(NSBundle *)getResourcesBundle
{
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:#"SparkSetup" withExtension:#"bundle"]];
return bundle;
}
+(UIStoryboard *)getSetupStoryboard
{
UIStoryboard *setupStoryboard = [UIStoryboard storyboardWithName:#"setup" bundle:[SparkSetupMainController getResourcesBundle]];
return setupStoryboard;
}
which seems to work well for finding the storyboard, but not for finding images in the .xcassets in the same bundle.
What am I doing wrong? how can I reference images from this storyboard/from code and be able to integrate this UI pod into any app?
Thanks!
At least as of version 1.0.1 of Cocoapods, image asset catalogs are supported.
In my Swift 4.2 code, I used:
public static var GoogleIcon: UIImage {
let bundle = Bundle(for: self)
log.msg("bundle: \(bundle)")
return UIImage(named: "GoogleIcon", in: bundle, compatibleWith: nil)!
}
In my pod spec, I used:
s.resources = "SMCoreLib/Assets/*.xcassets"
When I build using the simulator, the .car file does show up in the Framework:
cd /Users/<snip>/SMCoreLib.framework
ls
Assets.car Info.plist SMCoreLib
Well, image asset catalogs are not supported via pods - just include a resource bundle that contains your image files in your podspec like so:
s.subspec 'Resources' do |resources|
resources.resource_bundle = {'MyBundle' => ['Resources/**/*.{png}']}
end
and access the images safely from the pod like that:
+(NSBundle *)getResourcesBundle
{
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle bundleForClass:[self class]] URLForResource:#"MyBundle" withExtension:#"bundle"]];
return bundle;
}
+(UIImage *)loadImageFromResourceBundle:(NSString *)imageName
{
NSBundle *bundle = [MyClass getResourcesBundle];
NSString *imageFileName = [NSString stringWithFormat:#"%#.png",imageName];
UIImage *image = [UIImage imageNamed:imageFileName inBundle:bundle compatibleWithTraitCollection:nil];
return image;
}
That solves all issues of handling images/resources within a cocoapod.
if you are using swift.
class func loadImage(name: String) -> UIImage? {
let podBundle = NSBundle(forClass: MyClass.self)
if let url = podBundle.URLForResource("MyBundleName", withExtension: "bundle") {
let bundle = NSBundle(URL: url)
return UIImage(named: name, inBundle: bundle, compatibleWithTraitCollection: nil)
}
return nil
}
Swift 3 version
private func getImageFromBundle(name: String) -> UIImage {
let podBundle = Bundle(for: YourClass.self)
if let url = podBundle.url(forResource: "YourClass", withExtension: "bundle") {
let bundle = Bundle(url: url)
return UIImage(named: name, in: bundle, compatibleWith: nil)!
}
return UIImage()
}
Only this works for me (Swift 3 & Swift 4)
public struct ImagesHelper {
private static var podsBundle: Bundle {
let bundle = Bundle(for: YourClass.self)
return Bundle(url: bundle.url(forResource: "YourClass",
withExtension: "bundle")!)!
}
private static func imageFor(name imageName: String) -> UIImage {
return UIImage.init(named: imageName, in: podsBundle, compatibleWith: nil)!
}
public static var myImage: UIImage {
return imageFor(name: "imageName")
}
}
Then use it as below:
ImagesHelper.myImage
As in the #Chris Prince's answer, don't forget to update your podspec file like:
s.resource_bundles = {
'YourClass' => ['YourPodName/*/Assets.xcassets']
}
In you podspec do like this
s.resources = 'RootFolder/**/*.{lproj,storyboard,xcdatamodeld,xib,xcassets,json}'
You can access images from pods by using their bundle id
NSBundle * bundle = [NSBundle bundleWithIdentifier:#"bundle id of your pod"];
UIImage * image = [UIImage imageNamed:#"imageName" inBundle:bundle compatibleWithTraitCollection:nil];
I can't still get it to work, with none of the solutions above.
I have an Assets.xcassets file in my Pod.
And inside my Pod classes i would like to access images from the Assets
Specified like this:
s.resource_bundles = {
'SWDownloadPlayButton' => ['SWDownloadPlayButton/Assets/Assets.xcassets']
}
With this helper:
public struct ImagesHelper {
private static var podsBundle: Bundle {
let bundle = Bundle(for: SWDownloadPlayButton.self)
return Bundle(url: bundle.url(forResource: "SWDownloadPlayButton",
withExtension: "bundle")!)!
}
private static func imageFor(name imageName: String) -> UIImage {
return UIImage.init(named: imageName, in: podsBundle, compatibleWith: nil)!
}
public static var download_icon: UIImage {
return imageFor(name: "download_icon")
}
}
But whatever i do inside my Pod classes (not in the example project)...like this
var centerImage: UIImage = ImagesHelper.download_icon.withRenderingMode(.alwaysTemplate) {
didSet {
updateImage()
}
}
I still get a nil on the centerImage
Add your .xcassets file within the resources in your pod spec, and use the following UIImage init:
extension UIImage {
convenience init?(podAssetName: String) {
let podBundle = Bundle(for: ConfettiView.self)
/// A given class within your Pod framework
guard let url = podBundle.url(forResource: "CryptoContribute",
withExtension: "bundle") else {
return nil
}
self.init(named: podAssetName,
in: Bundle(url: url),
compatibleWith: nil)
}
}
My own solution to the issue with CocoaPods. Instead of mucking with UIImage, I added a method to Bundle instead. The method attempts to locate an inner bundle of a given name, but falls back to the original bundle. This allows the code to work in both the application code using pods, as well as the pod code itself.
extension Bundle {
/**
Locate an inner Bundle generated from CocoaPod packaging.
- parameter name: the name of the inner resource bundle. This should match the "s.resource_bundle" key or
one of the "s.resoruce_bundles" keys from the podspec file that defines the CocoPod.
- returns: the resource Bundle or `self` if resource bundle was not found
*/
func podResource(name: String) -> Bundle {
guard let bundleUrl = self.url(forResource: name, withExtension: "bundle") else { return self }
return Bundle(url: bundleUrl) ?? self
}
}
Then in the viewDidLoad method or wherever else you have your image setting code, put something like this, where "ClassFromPod" refers to a Swift class from a CocoaPod, and "ResourceName" is the name of the inner bundle within the pod (you should be able to see the bundles using Xcode project navigator in "Pods/Projects" -- embedded bundles have a LEGO icon and have a "bundle" suffix.)
super.viewDidLoad()
let bundle = Bundle(for: ClassFromPod.self).podResource(name: "ResourceName")
img1 = UIImage(named: "FancyBase", in: bundle, compatibleWith: nil)
img2 = UIImage(named: "FancyHandle", in: bundle, compatibleWith: nil)
As you can see, the UIImage API remains the same, only the bundle being used is different depending on what it finds at runtime.
If you're using SwiftUI, using the Image.init(_ name: String, bundle: Bundle?) initializer won't work, you have to initialize it like this:
func getImage(named name: String) -> UIImage {
// Use a random singleton class in your project.
// Emphasis on class, structs won't work.
let bundle = Bundle(for: [random class].self)
if let image = UIImage(named: name, in: bundle, compatibleWith: nil) {
return image
}
return .init()
}
Image(uiImage: getImage(named: "image_name"))
Combining a bit all the answers and making it easy to copy-paste in your project:
class RandomClass: Any { } // Necessary for UIImage extension below
extension UIImage {
convenience init?(fromPodAssetName name: String) {
let bundle = Bundle(for: RandomClass.self)
self.init(named: name, in: bundle, compatibleWith: nil)
}
}
Do not forget to add your asset catalogs to your .podspec file:
s.resource_bundles = {
'Resources' => ['YourPodName/*.xcassets']
}
Alright, I know this is new for everybody but I would think it'd be a simple concept - I am following this here to make a custom sticker message app extension:
https://code.tutsplus.com/tutorials/create-an-imessage-app-in-ios-10--cms-26870
Ive copied everything exactly and am trying to create a basic MSStickerBrowserView displaying (then later filtering using logic, but haven't attempted that yet) sticker pngs I have in my assets folder here:
The tutorial did not load from assets it seems but rather just from their project, regardless their code is old as here:
var stickers = [MSSticker]()
func loadStickers() {
for i in 1...2 {
if let url = Bundle.main.urlForResource("Sticker \(i)", withExtension: "png") { //ERROR!!
do {
let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "")
stickers.append(sticker)
} catch {
print(error)
}
}
}
}
I get the error
Bundle has no member URLforResource
I can't find anything on this. How can I just display my stickers programmatically in the app?
Error:
These are the images Im trying to load regardless of their name:
The reason that tutorial doesn't use asset catalogs is that you cannot get a valid fileURL for images placed in an .xcassets folder when calling the urlForResource method on the bundle.
You need to add your assets individually like you would other files you are bringing in to the app. Calling pathForResource or urlForResource on the bundle at that point will no longer return nil.
EDIT: Here is a function that will take a folder name, loop through it's contents and return [MSSticker]? based on what it finds
func createStickers(from folderName: String) -> [MSSticker]? {
guard
let path = Bundle.main.resourcePath
else { return nil }
var stickers = [MSSticker]()
let folderPath = "\(path)/\(folderName)"
let folderURL = URL(fileURLWithPath: folderPath)
//get a list of urls in the chosen directory
do {
let imageURLs = try FileManager.default.contentsOfDirectory(at: folderURL,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
//loop through the found urls
for url in imageURLs {
//create the sticker and add it, or handle error
do {
let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "yourDescription")
stickers.append(sticker)
} catch let error {
print(error.localizedDescription)
}
}
} catch let error {
print(error.localizedDescription)
}
//return nil if stickers array is empty
return stickers.isEmpty ? nil : stickers
}
This should let you just call this and get what you are after:
let stickers = createStickers(from: "YourFolderName")
Please note not to include the forward slash ('/') at the beginning of the folder name.
Just replace "resourceUrl" with:
let url = Bundle.main.url(forResource: "Sticker \(i)", withExtension: "png")
The code got replaced in Swift 3.
You can put the images in a folder like so (XCODE Viewport):
It make things more organised but doesnt need as much code as if you would put them in a .xcasset.
It can be put done by creating a new group instead of creating an .xcasset by (Right Clicking Message Extension and clicking New Group):
The following code for the StickerBrowserView can be called like so:
import UIKit
import Messages
class StickerBrowserViewController: MSStickerBrowserViewController {
var stickers = [MSSticker]()
func changeBrowserViewBackgroundColor(color: UIColor){
stickerBrowserView.backgroundColor = color
}
func loadStickers(){
createSticker(asset: "1", localizedDescription:"grinning face")
createSticker(asset: "2", localizedDescription:"grimacing face")
createSticker(asset: "3", localizedDescription:"grinning face with smiling eyes")
createSticker(asset: "4", localizedDescription:"face with tears of joy")
createSticker(asset: "5", localizedDescription:"smiling face with open mouth")
createSticker(asset: "6", localizedDescription:"smiling face with open mouth and smiling eyes")
}
func createSticker(asset: String, localizedDescription: String){
guard let stickerPath = Bundle.main.path(forResource:asset, ofType:"png") else {
print("couldn't create the sticker path for", asset)
return
}
// we use URL so, it's possible to use image from network
let stickerURL = URL(fileURLWithPath:stickerPath)
let sticker: MSSticker
do {
try sticker = MSSticker(contentsOfFileURL: stickerURL, localizedDescription: localizedDescription)
// localizedDescription for accessibility
stickers.append(sticker)
}catch {
print(error)
return
}
}
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int{
return stickers.count
}
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker{
return stickers[index] as MSSticker
}
}
(Ps. Not my blog, but found it on google and it has been very useful)
I want to convert this code into Swift. The Objective-C code here is making a singleton object(if I can describe as such). I can use dispatch_once_t to convert it, but I want to use a more elegant way which should be similar to "static let bundle: NSBundle!". But "static let bundle: NSBundle!" is not allowed in an extension since it doesn't allow stored properties.
So is it possible to convert the code without dispatch_once_t?
And I faced a problem that I can not have stored properties in a class extension
#implementation NSBundle (CTFeedback)
+ (NSBundle *)feedbackBundle
{
static NSBundle *bundle = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
NSBundle *classBundle = [NSBundle bundleForClass:[CTFeedbackViewController class]];
NSURL *bundleURL = [classBundle URLForResource:#"CTFeedback" withExtension:#"bundle"];
if (bundleURL) {
bundle = [NSBundle bundleWithURL:bundleURL];
} else {
bundle = [NSBundle mainBundle];
}
});
return bundle;
}
#end
My Swift Code:
extension NSBundle
{
static func feedbackBundle()-> NSBundle
{
static let bundle: NSBundle! //!! **Compiler Error here**
let classBundle = NSBundle.init(forClass: CTFeedbackViewController.self)
let bundleURL = classBundle.URLForResource("CTFeedback", withExtension: "bundle")
if let bundleURL2 = bundleURL
{
bundle = NSBundle(URL: bundleURL2)
}
else
{
bundle = NSBundle.mainBundle()
}
return bundle;
}
}
Update:
Thanks to people's answerings. I do it like this now. I am not sure it's the best way/
private class FeedbackBundle
{
static let classBundle = NSBundle.init(forClass: CTFeedbackViewController.self)
}
extension NSBundle
{
static func feedbackBundle()-> NSBundle
{
let bundleURL = FeedbackBundle.classBundle.URLForResource("CTFeedback", withExtension: "bundle")
if let bundleURL2 = bundleURL
{
return NSBundle(URL: bundleURL2)!
}
else
{
return NSBundle.mainBundle()
}
}
}
In Swift, you cannot add static variables in extensions.
You can retry in the original class if accessible. Otherwise, you can modify the code like:
if let bundleURL2 = bundleURL
{
return NSBundle(URL: bundleURL2)
}
else
{
return NSBundle.mainBundle()
}
You could always have the static var reside outside of the extension, either in a separate class or as a simple global variable (which static variables are anyway).
for example:
private class FeedbackBundle
{
static var bundle: NSBundle!
}
extension NSBundle
{
static func feedbackBundle()-> NSBundle
{
let classBundle = NSBundle.init(forClass: CTFeedbackViewController.self)
let bundleURL = classBundle.URLForResource("CTFeedback", withExtension: "bundle")
if let bundleURL2 = bundleURL
{
FeedbackBundle.bundle = NSBundle(URL: bundleURL2)
}
else
{
FeedbackBundle.bundle = NSBundle.mainBundle()
}
return FeedbackBundle.bundle;
}
}
You can also subclass NSBundle and add this property to it.
I have a cocoapod library which contains assets in 2 formats:
a .storyboard
XCode asset catalog .xcassets (with images)
my podspec file contains the definition for the resource bundle:
s.resource_bundle = {'SparkSetup' => ['Resources/**/*.{xcassets,storyboard}']}
and I have a separate target in the pod project to create a resource bundle by using those files + a plist file for that bundle.
thing is that when I use the pod in an app project - I can see the storyboard/xcassets files being in the pod target and I can access and run the storyboard easily but the images referenced in the storyboard (to the .xcassets file) are not found in run-time (but displayed correctly in IB).
Error I get is:
Could not load the "spinner" image referenced from a nib in the bundle with identifier "(null)"
I do see a bundle file in the products directory.
To instanciate VCs in the storyboard I use:
+(NSBundle *)getResourcesBundle
{
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:#"SparkSetup" withExtension:#"bundle"]];
return bundle;
}
+(UIStoryboard *)getSetupStoryboard
{
UIStoryboard *setupStoryboard = [UIStoryboard storyboardWithName:#"setup" bundle:[SparkSetupMainController getResourcesBundle]];
return setupStoryboard;
}
which seems to work well for finding the storyboard, but not for finding images in the .xcassets in the same bundle.
What am I doing wrong? how can I reference images from this storyboard/from code and be able to integrate this UI pod into any app?
Thanks!
At least as of version 1.0.1 of Cocoapods, image asset catalogs are supported.
In my Swift 4.2 code, I used:
public static var GoogleIcon: UIImage {
let bundle = Bundle(for: self)
log.msg("bundle: \(bundle)")
return UIImage(named: "GoogleIcon", in: bundle, compatibleWith: nil)!
}
In my pod spec, I used:
s.resources = "SMCoreLib/Assets/*.xcassets"
When I build using the simulator, the .car file does show up in the Framework:
cd /Users/<snip>/SMCoreLib.framework
ls
Assets.car Info.plist SMCoreLib
Well, image asset catalogs are not supported via pods - just include a resource bundle that contains your image files in your podspec like so:
s.subspec 'Resources' do |resources|
resources.resource_bundle = {'MyBundle' => ['Resources/**/*.{png}']}
end
and access the images safely from the pod like that:
+(NSBundle *)getResourcesBundle
{
NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle bundleForClass:[self class]] URLForResource:#"MyBundle" withExtension:#"bundle"]];
return bundle;
}
+(UIImage *)loadImageFromResourceBundle:(NSString *)imageName
{
NSBundle *bundle = [MyClass getResourcesBundle];
NSString *imageFileName = [NSString stringWithFormat:#"%#.png",imageName];
UIImage *image = [UIImage imageNamed:imageFileName inBundle:bundle compatibleWithTraitCollection:nil];
return image;
}
That solves all issues of handling images/resources within a cocoapod.
if you are using swift.
class func loadImage(name: String) -> UIImage? {
let podBundle = NSBundle(forClass: MyClass.self)
if let url = podBundle.URLForResource("MyBundleName", withExtension: "bundle") {
let bundle = NSBundle(URL: url)
return UIImage(named: name, inBundle: bundle, compatibleWithTraitCollection: nil)
}
return nil
}
Swift 3 version
private func getImageFromBundle(name: String) -> UIImage {
let podBundle = Bundle(for: YourClass.self)
if let url = podBundle.url(forResource: "YourClass", withExtension: "bundle") {
let bundle = Bundle(url: url)
return UIImage(named: name, in: bundle, compatibleWith: nil)!
}
return UIImage()
}
Only this works for me (Swift 3 & Swift 4)
public struct ImagesHelper {
private static var podsBundle: Bundle {
let bundle = Bundle(for: YourClass.self)
return Bundle(url: bundle.url(forResource: "YourClass",
withExtension: "bundle")!)!
}
private static func imageFor(name imageName: String) -> UIImage {
return UIImage.init(named: imageName, in: podsBundle, compatibleWith: nil)!
}
public static var myImage: UIImage {
return imageFor(name: "imageName")
}
}
Then use it as below:
ImagesHelper.myImage
As in the #Chris Prince's answer, don't forget to update your podspec file like:
s.resource_bundles = {
'YourClass' => ['YourPodName/*/Assets.xcassets']
}
In you podspec do like this
s.resources = 'RootFolder/**/*.{lproj,storyboard,xcdatamodeld,xib,xcassets,json}'
You can access images from pods by using their bundle id
NSBundle * bundle = [NSBundle bundleWithIdentifier:#"bundle id of your pod"];
UIImage * image = [UIImage imageNamed:#"imageName" inBundle:bundle compatibleWithTraitCollection:nil];
I can't still get it to work, with none of the solutions above.
I have an Assets.xcassets file in my Pod.
And inside my Pod classes i would like to access images from the Assets
Specified like this:
s.resource_bundles = {
'SWDownloadPlayButton' => ['SWDownloadPlayButton/Assets/Assets.xcassets']
}
With this helper:
public struct ImagesHelper {
private static var podsBundle: Bundle {
let bundle = Bundle(for: SWDownloadPlayButton.self)
return Bundle(url: bundle.url(forResource: "SWDownloadPlayButton",
withExtension: "bundle")!)!
}
private static func imageFor(name imageName: String) -> UIImage {
return UIImage.init(named: imageName, in: podsBundle, compatibleWith: nil)!
}
public static var download_icon: UIImage {
return imageFor(name: "download_icon")
}
}
But whatever i do inside my Pod classes (not in the example project)...like this
var centerImage: UIImage = ImagesHelper.download_icon.withRenderingMode(.alwaysTemplate) {
didSet {
updateImage()
}
}
I still get a nil on the centerImage
Add your .xcassets file within the resources in your pod spec, and use the following UIImage init:
extension UIImage {
convenience init?(podAssetName: String) {
let podBundle = Bundle(for: ConfettiView.self)
/// A given class within your Pod framework
guard let url = podBundle.url(forResource: "CryptoContribute",
withExtension: "bundle") else {
return nil
}
self.init(named: podAssetName,
in: Bundle(url: url),
compatibleWith: nil)
}
}
My own solution to the issue with CocoaPods. Instead of mucking with UIImage, I added a method to Bundle instead. The method attempts to locate an inner bundle of a given name, but falls back to the original bundle. This allows the code to work in both the application code using pods, as well as the pod code itself.
extension Bundle {
/**
Locate an inner Bundle generated from CocoaPod packaging.
- parameter name: the name of the inner resource bundle. This should match the "s.resource_bundle" key or
one of the "s.resoruce_bundles" keys from the podspec file that defines the CocoPod.
- returns: the resource Bundle or `self` if resource bundle was not found
*/
func podResource(name: String) -> Bundle {
guard let bundleUrl = self.url(forResource: name, withExtension: "bundle") else { return self }
return Bundle(url: bundleUrl) ?? self
}
}
Then in the viewDidLoad method or wherever else you have your image setting code, put something like this, where "ClassFromPod" refers to a Swift class from a CocoaPod, and "ResourceName" is the name of the inner bundle within the pod (you should be able to see the bundles using Xcode project navigator in "Pods/Projects" -- embedded bundles have a LEGO icon and have a "bundle" suffix.)
super.viewDidLoad()
let bundle = Bundle(for: ClassFromPod.self).podResource(name: "ResourceName")
img1 = UIImage(named: "FancyBase", in: bundle, compatibleWith: nil)
img2 = UIImage(named: "FancyHandle", in: bundle, compatibleWith: nil)
As you can see, the UIImage API remains the same, only the bundle being used is different depending on what it finds at runtime.
If you're using SwiftUI, using the Image.init(_ name: String, bundle: Bundle?) initializer won't work, you have to initialize it like this:
func getImage(named name: String) -> UIImage {
// Use a random singleton class in your project.
// Emphasis on class, structs won't work.
let bundle = Bundle(for: [random class].self)
if let image = UIImage(named: name, in: bundle, compatibleWith: nil) {
return image
}
return .init()
}
Image(uiImage: getImage(named: "image_name"))
Combining a bit all the answers and making it easy to copy-paste in your project:
class RandomClass: Any { } // Necessary for UIImage extension below
extension UIImage {
convenience init?(fromPodAssetName name: String) {
let bundle = Bundle(for: RandomClass.self)
self.init(named: name, in: bundle, compatibleWith: nil)
}
}
Do not forget to add your asset catalogs to your .podspec file:
s.resource_bundles = {
'Resources' => ['YourPodName/*.xcassets']
}