Loading a resource (e.g. storyboard) in a Swift framework - ios

I'm trying to make a framework that will bundle a storyboard. I have checked, and the created .framework includes my .storyboard file (as a .storyboardc file), and have gotten the framework to load the storyboard at runtime. However, this being a framework, I'd like for the code to be as versatile as possible, and I feel like my current solution is a little hacky. Currently, I'm loading the storyboard using the following code:
let mainBundlePath: String = NSBundle.mainBundle().resourcePath
let frameworkBundlePath = mainBundlePath.stringByAppendingPathComponent("Frameworks/AuthenticationManager.framework")
let frameworkBundle = NSBundle(path: frameworkBundlePath)
let storyboard = UIStoryboard(name: "Storyboard", bundle: frameworkBundle)
A couple of things I've noticed that could be gotchas:
The path of the framework (Frameworks/) could, potentially, change in the future, and should not relied upon?
The name of the framework could change, but without first getting the NSBundle of the framework there's no way of getting the product name?
Frameworks aren't really bundles (?), so loading them as a bundle could have unforeseen consequences in the future?
There may be other issues, solutions to the above issues, or the above issues may not be issues after all, but I've not been able to think of them.
This question may be be more suited for Code Review, but I felt it'd fit in well here, too. If it needs to be moved over there, feel free to do that or tell me to do it.

A framework (created from the "Cocoa Touch Framework" template) has a bundle identifier,
which is stored in the Info.plist of the generated framework bundle.
The bundle identifier is configured in the general target settings.
By default, it is <organization prefix>.<framework name>.
You can locate the framework using this identifier, for example:
let frameworkBundle = NSBundle(identifier: "com.duffy.AuthenticationManager")
let storyboard = UIStoryboard(name: "Storyboard", bundle: frameworkBundle)

Here's a cleaner solution (no framework identifiers involved):
let storyboardName = "StoryboardName"
let storyboardBundle = NSBundle(forClass: self)
let storyboard = UIStoryboard(name: storyboardName, bundle: storyboardBundle)
If this code is not inside of a static method, use YourClass.self instead of self.
Example for Swift 3:
let storyboardBundle = Bundle(for: NumberPadViewController.self)
super.init(nibName: "NumberPadViewController", bundle: storyboardBundle)

For Swift 3/4, it's now simply Bundle :
let bundle = Bundle(identifier: "com.myframework.id")
let storyboard = UIStoryboard(name: "FrameworkMain", bundle: bundle)

This is a much better way to load the bundle in Swift 3 without the need of passing a string that is much more likely to fail
let myBundle = Bundle(for: MyViewController.self)
let myStoryboard = UIStoryboard(name: "StoryboardName", bundle: myBundle)

Related

Why can I reach Storyboard_a.storyboard and fail to reach Storyboard_b.storyboard in my framework?

I have a framework with two storyboards in it: StoryBoard_A.storyboard and StoryBoard_B.storyboard.
I can reach StoryBoard_A but not StoryBoard_B
I use my framework as a pod in my main project.
in my framework podspec file I have:
s.source_files = "myFramework/**/*.{swift}"
s.resource_bundles = { 'myFramework' => ['myFramework/**/*.{storyboard,xib,xcassets}'] }
I know both storyboards are in myFramework bundle because:
In myFramework Build Phases, under Copy Bundle Resources I can see them both included.
In myFramework.framework I can see: StoryBoard_A.storyboardc and StoryBoard_B.storyboardc
When I 'pod install' myFramework as a development pod I can see both storyboards in the Project navigator of the main project
In myFramework, from ViewController_1 I initiate ViewController_a from StoryBoard_A.storyboard and ViewController_b from StoryBoard_B.storyboard.
I use the same technique:
let podBundle = Bundle(for: ViewController_1.self)
let bundleURL = podBundle.url(forResource: "myFramework", withExtension: "bundle")
let bundle = Bundle(url: bundleURL!)!
let storyBoard = UIStoryboard(name: "StoryBoard_A", bundle: bundle)
let viewController_a = storyBoard.instantiateViewController(withIdentifier: "ViewController_a_id") as? ViewController_a
but when I do:
let storyBoard = UIStoryboard(name: "StoryBoard_B", bundle: bundle)
let viewController_b = storyBoard.instantiateViewController(withIdentifier: "ViewController_b_id") as? ViewController_b
the app crashes in the second line with the following error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'StoryBoard_B' in bundle...
What am I missing?
Thanks
Right click your app under Products in Xcode and explore it in Finder. Just check whether StoryBoard_B.storyboardc files are present.If files are not present, you have a different problem to solve.
If files are present, try accessing it like shown below.
let podBundle = Bundle(forClass: ViewController_1.self)
let bundleURL = podBundle.resourceURL?.URLByAppendingPathComponent("myFramework.bundle")
let resourceBundle = Bundle(URL: bundleURL!)
I found the problem:
Since this was a piece of code that I inherited and I tried to merge it into myFramework I wasn't aware of the fact that inside ViewController_b.swift (the one that I was trying to initiate from the storyboard) there was:
var storyboard = UIStoryboard(name: "Stroyboard_B", bundle: nil)
in the class scope.
I changed this to the bundle I created the same way as described above and that solved the problem.
I found this after I successfully tried to initiate other ViewControllers from this storyboard. That made me look inside ViewController_b to see how it defers than other view controllers

Using nibs from cocoapods in swift 4

I have made a framework that contains a function that displays a login view according to a xib that is contained in that framework. Both the xib file and the swift file for the view are called AuthenticationViewController
However when I try to use this function in another project that uses this as a pod, it fails with "Could not load NIB in bundle...(not yet loaded)' with name 'AuthenticationViewController'"
The view is being shown by the following code that is located in my pod/framework:
func authenticate(viewController: UIViewController){
let bundle = Bundle(for:AuthenticationViewController.self)
let newViewController = AuthenticationViewController(nibName:"AuthenticationViewController" , bundle: bundle)
viewController.present(newViewController, animated: true, completion: nil)
}
What is the issue here? Is there supposed to be a separate bundle for my pod, because I only get one bundle when calling:
Bundle.allBundles
My .podspec file contains the following section:
s.resource_bundles = {
"MyPodName" => ["MyPodName/*.xib"]
}
but I have tried to load the bundle using:
Bundle(identifier:"MyPodName")
and that does not work either.
How are you supposed to use nibs from pods?
The problem was that I was using:
s.resource_bundles = {
"MyPodName" => ["MyPodName/*.xib"]
}
This does not seem to work with the approach i had done. So I had to change podspec file to:
s.resources = ["MyPodName/*.xib"]
That made everything work
I seem that the bundle is not the correct one. I use this function in my pod library in order to get the bundle
- (NSBundle *)getBundle {
NSBundle *podBundle = [NSBundle bundleForClass:self.classForCoder];
NSURL *podBundleURL = [podBundle URLForResource:#"MyPodName" withExtension:#"bundle"];
NSBundle *bundle = [[NSBundle alloc] initWithURL:podBundleURL];
return bundle;
}
Once you have the bundle you can load the view. Also, run a pod install after you edit the .podspec and add a new file to reorganize the files.

'UIViewController' to 'WWCalendarTimeSelector.VNClockViewController'

I get this error when trying to instantiate a viewcontroller from a storyboard.
I added a pod into my test project WWCalendarTimeSelector
and edited the pod. I added new files VNClockViewController.swift and VNClockViewController.storyboard.
In my VNClockViewController.swift, I have this:
open static func instantiate() -> VNClockViewController {
let podBundle = Bundle(for: VNClockViewController.self)
let bundleURL = podBundle.url(forResource: "WWCalendarTimeSelectorStoryboardBundle", withExtension: "bundle")
var bundle: Bundle?
if let bundleURL = bundleURL {
bundle = Bundle(url: bundleURL)
}
return UIStoryboard(name: "VNClockViewController", bundle: bundle).instantiateInitialViewController() as! VNClockViewController //This line causes the error
}
but when I try to instantiate this Viewcontroller in my Project, I get the error
Could not cast value of type 'UIViewController' (0x10326cca8) to 'WWCalendarTimeSelector.VNClockViewController' (0x1008122d0).`
Please tell me if you need to see more code.
EDIT:
I'm using my fork of the Pods from github.
Please use this pod in a sample project to see the error.
pod 'WWCalendarTimeSelector', :git => 'https://github.com/binsnoel/WWCalendarTimeSelector.git'
Try to instantiate VNClockViewController.instantiate() and see the error.
I had faced same issue with same error, It's happening because of bug in Library
Steps to solve the issue
1) First goto Pod -> WWCalendarTimeSelector -> Resource folder
2) Select WWCalendarTimeSelector.storyboard and check in View hierarchy and Select Clock View as in image.
3) Now, got to Identity Inspector and You see Custom class have missing Module. So set WWCalendarTimeSelector as like below image
4) Done, Check and run.

Nib load failure when presenting its view controller

I have this function in a class that inherits from NSObject:
open func showCustomDialogInView(vc: UIViewController) {
let bundle = Bundle(for: CustomDialogViewController.self)
let customDialog = CustomDialogViewController(nibName: "CustomDialogViewController", bundle: bundle)
customDialog.delegate = vc
customDialog.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
vc.present(customDialog, animated: true, completion: nil)
}
I had this working in one of my iOS projects before updating to Xcode 8 and Swift 3 language, but now when I run the app I get a crash when vc.present(customDialog, animated: true, completion: nil) line is reached. I get this message in log console:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not load NIB in bundle: 'NSBundle /MyApp.app> (loaded)' with name 'CustomDialogViewController''
I don't understand what it is happening, since let customDialog = CustomDialogViewController(nibName: "CustomDialogViewController", bundle: bundle) line doesn't crash and it seems that I get a CustomDialogViewController object.
Does somebody could help me with this issue? Thanks
This error can occur when you rename files outside of XCode. To solve it you can just remove the files from your project (Right Click - Delete and "Remove Reference").
Then after you can re-import the files in your project and everything will be OK.
It looks like in this case this error was because for some reason the xib file was missing the appropriate target membership.
Thanks for your answers

How to load an image in prepareForInterfaceBuilder with a IBDesignable UIImageView

I would like to load a sample image in an IB designable UIImageView, to be shown in Interface Builder while editing the interface. The following code does not work, as the view placeholder in IB remains empty (the view area contains only the UIImageView text):
#IBDesignable
class TestImageView : UIImageView
{
override func prepareForInterfaceBuilder() {
//let bundle = NSBundle.mainBundle()
let bundle = NSBundle(forClass: nil)
let imagePath = bundle.pathForResource("Test", ofType: "jpg")
self.image = UIImage(contentsOfFile: imagePath)
}
}
Note that:
in IB the Custom Class class for the view is correct (TestImageView)
Test.jpg is present in the project (if I manually set the image property of the UIImageView in IB the image shows up).
I tried the two different methods of getting the bundle present in the code
This was tested with Xcode 6 beta 3.
Update: in both cases the bundle path I get is "/Applications/Temporary/Xcode6-Beta3.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays". In that path the image is obviously not present.
Try getting the bundle of the class like this:
let bundle = NSBundle(forClass: self.dynamicType)
or specifying the class name like this
let bundle = NSBundle(forClass: TestImageView.self)
Assuming that your image is in the bundle, for example Images.xcassets, you can then load it using:
self.image = UIImage("Test", inBundle: bundle, compatibleWithTraitCollection: self.traitCollection)
Remember to check whether your image is nil before trying to use it. I have not been able to get the image path using bundle.pathForResource to work correctly with normal image assets. There also doesn't appear to be a UIImage call where you specify just the name and bundle, so you have to use trait collection.
This question is related to:
xcode 6 IB_DESIGNABLE- not loading resources from bundle in Interface builder
Response from Apple...
Engineering has determined that this issue behaves as intended based
on the following:
We can't really make this any easier than specifying the bundle. You
might say, "oh, let's swizzle -[NSBundle mainBundle]", but lots of
call sites that reference a bundle don't go through there (or go
through the CF API). One might say then "ok, well then how about we at
least swizzle -[UIImage imageNamed:]". The problem here is that there
is no single replacement for the main bundle. You might have multiple
live view bundles (either frameworks or apps) loaded in at once, so we
can't just pick one to be the main bundle.
Developers need to be aware of bundles and how to get images from a
bundle. Developers should be using
UIImage(named:inBundle:compatibleWithTraitCollection:) for all image lookups.
Updated for Swift 4.2
When you instantiate an UIImage (with UIImage(named : "SomeName") the app will look for the asset in your main bundle, which works fine usually. But when you are at design time, the InterfaceBuilder holds the code of the designable views (for compiling while designing) in a separate bundle.
So the solution is: Define your bundle dynamically, hence your files can be found in design, compile and run time:
// DYNAMIC BUNDLE DEFINITION FOR DESIGNABLE CLASS
let dynamicBundle = Bundle(for: type(of: self))
// OR ALTERNATIVELY BY PROVDING THE CONCRETE NAME OF YOUR DESIGNABLE VIEW CLASS
let dynamicBundle = Bundle(for: YourDesignableView.self)
// AND THEN SUCCESSFULLY YOU CAN LOAD THE RESSOURCE
let image = UIImage(named: "Logo", in: dynamicBundle, compatibleWith: nil)
Lets pop in the swift 3 answer
let bundle = Bundle(for: self.classForCoder)
... UIImage(named: "AnImageInYourAssetsFolderPerhaps", in: bundle, compatibleWith: self.traitCollection)!
For Swift 4 (and 3) use this:
let image = UIImage(named: "foo", in: Bundle(for: type(of: self)), compatibleWith: traitCollection)
This approach gets the bundle universally
I was able to fix the issue getting the Interface Builder project path from the current NSProcessInfoobject. You can then gather the correct path from the IB_PROJECT_SOURCE_DIRECTORIESkey.
override func prepareForInterfaceBuilder() {
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as String
let imagePath = firstPath.stringByAppendingPathComponent("PrepareForIBTest/Test.jpg")
let image = UIImage(contentsOfFile: imagePath)
self.image = image
}
}
This technique is described in the WWDC 2014 411 session "What's New in Interface Builder" as suggested by bjhomer in this Apple Developer Forums post.
Moreover, I need to say that it was required to switch to a UIView subclass, because it seems that the appereance of the UIImageViewdoes not change in live views.
This will load your IB_Designable, or if you have none - the default image.
- (void)prepareForInterfaceBuilder
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
imagePic = imagePic ? [imagePic imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : [UIImage imageNamed:#"goodIcon" inBundle:bundle compatibleWithTraitCollection:self.traitCollection];
}
For Swift 2 and Xcode 7, the interface has been changed. Should use
let bundle = NSBundle(forClass: self.dynamicType)
let image = UIImage(named: "imageName", inBundle: bundle, compatibleWithTraitCollection: self.traitCollection)
let imageView = UIImageView(image: image)
I use it in my project, it works fine for both IB and device.

Resources