loading a custom UIView from a nib either causes an infinite loop or throws a nil exception - swift

I have created a custom view (ImageAndToolBarContainerView) with a corresponding XIB file that I would like to load into multiple UIViewControllers in my app.
I have been hunting for a proper tutorial of how to do this, but almost every one I've come across either is too old or causes major exceptions.
When I try loading it through the story board / NIB, (I create a view in the UIViewController on the storyboard and I set the "Class" attribute to ImageAndToolBarContainerView. I Set the outlets, including the View, but none of them seem to load when the class is called, and I get the error :
fatal error: unexpectedly found nil while unwrapping an Optional value
when I try to access the view:
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
//setup()
self.view.frame = self.bounds
self.addSubview(view)
}
Then, I try using the following code to load the NIB instead (by uncommenting the setup() function above. The code of setup is this:
func setup()
{
self.loadViewFromNIB()
self.view.frame = self.bounds
self.addSubview(view)
}
func loadViewFromNIB() -> UIView
{
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "ImageAndToolBarContainer", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
That of course leads to an infinite loop.
I also have the following function declares:
override func awakeFromNib()
{
super.awakeFromNib()
self.view.frame = self.bounds
self.addSubview(view)
}
What am I missing here? Where did I go wrong? Is there a definitive tutorial in how to do this properly?

The infinite loop is probably caused because you set the File's Owner and also subclassed the View's class to your View's class name.

For anyone having this issue, if you register a Nib for a table or collection view, you are telling that parent view to go and load a nib named 'x' whenever it needs to dequeue a cell.
Normally with Nib code, you'll want a method to go and load the actual XML that makes your layout to tie it to the Nib class, but when you register a reusable view, your registration means that the parent view is responsible for doing this. It will literally go and load an XML file for you and try and tie that to a class when you cast it. By adding another loadViewFromNIB call inside of this will then cause an infinite loop to happen.
All you need to do is set the cell Nib's class to your custom class, register it and it will do the rest for you with reusable cells - remove the loadViewFromNIB method from any initialisers inside your cell and add registration code on the collection or table view class.

Related

Instantiate UIView from nib causes infinite loop issue

I'm trying to use a custom view I created.
I use instantiation from nib, but it causes an infinite loop which I'm not sure how to fix. Any idea?
Here is the image of the run result:
And here's the code that causes the issue:
// MARK: - Init & Setup
// Needed for IBDesignable
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleHeight.rawValue)))
addSubview(view)
}
func loadViewFromNib() -> UIView{
let bundle = Bundle(for:type(of: self))
let nib = UINib(nibName: "LoginView", bundle: bundle) // TEST: changin bundle from bundle-> nil
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
Edit: Here's an image of the connection
Thanks :)
Quick answer:
Since the top-level view in the Xib had its custom class set to YourCustomeView, the xib loading process loadViewFromNib will then call your initWithCoder method again ⛔️🚫♽.
Quick fix:
In the Xib, instead of setting the custom class of the view as YourCustomeView, set YourCustomeView as The File Owner of the Xib.
More info
https://medium.com/#anandin02/loading-custom-views-in-ios-the-right-way-bedfc06a4fbd
It is not clear from your example (please next time provide the whole file, not just parts of it), but it looks like you are trying to instantiate LoginView from itself.
init() -> setup() -> loadViewFromNib() -> init() -> ...
The problem is that the instantiate will call the LoginView's init() method, which will again call loadViewFromNib(). I hope you see the problem now.
What are you trying to do with loadViewFromNib?
Edit: In the top right corner of your second image there is a "Custom class" section. There, in the "class" field you have to provide the name of your controller (LoginViewController or something like that). This way you connect your view and controller, and you don't need that custom loadViewFromNib method that causes your problem.
For anyone having this issue, if you register a Nib for a table or collection view, you are telling that parent view to go and load a nib named 'x' whenever it needs to dequeue a cell.
Normally with Nib code, you'll want a method to go and load the actual XML that makes your layout to tie it to the Nib class, but when you register a reusable view, your registration means that the parent view is responsible for doing this. It will literally go and load an XML file for you and try and tie that to a class when you cast it. By adding another loadNib call inside of this will then cause an infinite loop to happen.
All you need to do is set the cell Nib's class to your custom class, register it and it will do the rest for you with reusable cells - remove the loadNib method from any initialisers inside your cell and add registration code on the collection or table view class.

Make reusable component in Xcode storyboard

I have a specific situation but what I'm looking for is a generic solution. Currently I have a UIImageView that contains an image, a few labels, and multiple levels of constraints. I would like to configure this set of controls' properties once and reuse them inside of multiple controllers. Such that if I have to update this set, I would do it in one place and all the controller instances would get the change (sort of like how Sketch works with symbols).
You, sir, need a custom View!
My typical approach for this is to create an xib file, design the view I need, and create a class that subclasses UIView.
When you do this, you can assign the class of the xib File's Owner (in interface builder) and link up any #IBOutlets from the view to your custom class.
For the class, you'll need to implement a few methods. Here is an example custom view:
class LoadingView: UIView {
#IBOutlet var view: UIView!
#IBOutlet weak var messageLabel: UILabel!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
setUpView()
}
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
setUpView()
}
func setUpView() {
self.view.layer.cornerRadius = 10.0
self.view.layer.masksToBounds = true
}
private func loadViewFromNib() {
let bundle = Bundle.init(for: self.classForCoder)
bundle.loadNibNamed("LoadingView", owner: self, options: nil)
self.view.frame = bounds
self.addSubview(self.view)
}
}
You are required (pun intended) to implement the required init and the override init methods, and the other two are (kind of) optional. The loadViewFromNib is a convenience method that implements the logic to actually load the xib file from your app's bundle.
Don't forget to match the nib name with your xib file name! You'll thank me later. ;)
You can use this view in storyboards and use constraints, etc. by placing a regular old view and assigning its class to your custom class.
You can also play around with #IBDesignable to actually see your custom view in interface builder, though it tends to constantly reload and slow down Xcode unless you toggle a setting that I can't remember the name of right now (sorry!).
Enjoy!
What you want is not possible exactly in the way you describe it but there's a way to achieve the same result.
Create a subclass of UIView that will contain all the content you want, once you do that there are two options.
The first (and best, imo) option is to generate your layout with code when the view is initialized. This will allow you to add the view to other view controllers and it will initialize itself. The downside of this method is that you'll need to create the constraints with code.
The second option is to create a xib with your views and constraints and initialize your custom class from that xib. The downside of this is that you'll have to instantiate your view with code and place it in the view hierarchy yourself. You could create a container in the storyboard where you will add the view and pin it to the edges.

view.endEditing causing app to freeze on some textFields

Problem solved. See end of post.
Sorry if this is a bit long but I'm hoping I've included as much info to get this solved.
Brief overview of problem: Enter value in a textField using my custom keypad. Tap done button(should trigger view.endEditing) and some textFields will cause the app to freeze, most the time Xcode won't even throw an error but instead just restart the app, but i did catch one once(pic below). It works as expected on some textFields.
So I have a view controller with a bunch of textFields for the user to fill out which then performs calculations.
I have made a custom Keypad which essentially is the decimal pad with a "Done" button. I did this by making an keyboard.xib file and a keyboard.swift file.
Heres a snapshot of the error, I've included a whole bunch of my code below incase I'm using a method that isn't the best.
This is how the keyboard.swift file looks:
import UIKit
// The view controller will adopt this protocol (delegate)
// and thus must contain the keyWasTapped method
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
func backspace()
}
class keyboard: UIView {
// This variable will be set as the view controller so that
// the keyboard can send messages to the view controller.
weak var delegate: KeyboardDelegate?
// MARK:- keyboard initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
let xibFileName = "Keyboard" // xib extention not included
let view = NSBundle.mainBundle().loadNibNamed(xibFileName, owner: self, options: nil)[0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
// MARK:- Button actions from .xib file
#IBAction func keyTapped(sender: UIButton) {
// When a button is tapped, send that information to the
// delegate (ie, the view controller)
self.delegate?.keyWasTapped(sender.titleLabel!.text!) // could alternatively send a tag value
}
#IBAction func backspace(sender: UIButton) {
self.delegate?.backspace()
}
#IBAction func Done(sender: UIButton) {
self.delegate?.keyDone()
}
}
In the viewController I'm pretty sure I've included all the necessary things to access the keyboard seeing as it works for some textFields. Such as:
class myViewController: UITableViewController,UITextFieldDelegate, KeyboardDelegate
Then in viewDidLoad set each textField delegate:
self.textField1.delegate = self
self.textField2.delegate = self
self.textField3.delegate = self
// initialize custom keyboard
let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: numpad.height))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
textField1.inputView = keyboardView
textField2.inputView = keyboardView
textField3.inputView = keyboardView
Then this function (which seems to me to be the problem):
func keyDone() {
view.endEditing(true)
//activeTextField.resignFirstResponder()
print("please dont freeze")
}
I have checked all the connections, they seem to be fine.
Let me know if I can add any more info to help work it out.
Many Thanks.
Solved!!!
I suppose ill just put it down to beating my head over it rather than taking a break from the screen! Still I'm confused why it wasn't given a more specific error.
The problem was that in some cases one of the functions was dividing by zero (this is undefined... not possible) but a good thing to take from this(thank you Olivier) is the Instruments Tools to help find where abouts the code was losing its mind. This tutorial helped me understand how to use instruments! So once I could see where it was going crazy I set up a bunch of print statements to watch the values as they went into the 'problem' calculation, where I found the denominator to be zero. Bit of rearranging the code around to avoid this and problem solved!
This error message is basically saying that there is a memory issue, try running the code with instruments (Allocations in particular) this might reveal is there is something amiss with your keyboard
Edit 2: for anyone finding this error message in future (actual solution in this case)
Double check any code code running after keyDone() to see if there are any infinite loops or situations that would cause the compiler to assume an infinite amount of memory is required. In this case a line of code was dividing by zero, causing a fatal memory error (unable to allocate the N/A value it generated)

Swift: Reusable UIView in storyboard and sizing constraints

I'm trying to create a reusable UIView in Swift that I can plug into my Storyboard view controllers. My key issue right now is that the reusable UIView "widget" doesn't fully fit into the UIView box in the storyboard. I followed this tutorial to set up the reusable UIView widget
Created a subclass of UIView and a corresponding .xib -- and connected these:
import UIKit
class MyWidgetView: UIView {
#IBOutlet var view: UIView!;
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
NSBundle.mainBundle().loadNibNamed("MyWidgetView", owner: self, options: nil);
self.addSubview(self.view);
}
}
In the XIB, which is the interface file corresponding to the code above, I used UIView with Freeform size under the Simulated Metrics, and Scale to Fill under View mode.
In the main storyboard, I added a UIView block (same rectangular shape) and changed the Class to MyWidgetView
It works, but the components I created in the XIB look squished in the actual app, despite the fact that I used layout constraints in both the XIB and also the main storyboard.
See the screenshot. The pink part isn't supposed to appear, since that is just a color of the UIVIew on the main storyboard that I added to test the sizing. That UIView is actually MyWidgetView (after I changed the class in step 3. So in theory, since MyWidgetView == the UIView on the main storyboard, and that UIView has constraints that make it rectangular in the superview, then why is my widget squished? The blue part below should extend all the way right.
The actual view hierarchy loaded from the nib file in your code is added via
self.addSubview(self.view). So, the frame of your self.view actually has no relationship with its parent, i.e. MyWidgetView.
You may choose either adding layout constraints through code or just setting its frame after being added as a subview. Personally, I prefer the latter. In my experiment, the following is what works for me. I am using Xcode 6.4, which I think is not the same one as yours.
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if let nibsView = NSBundle.mainBundle().loadNibNamed("MyWidgetView", owner: self, options: nil) as? [UIView] {
let nibRoot = nibsView[0]
self.addSubview(nibRoot)
nibRoot.frame = self.bounds
}
}
Alternatively the variable frame can be overridden. This code worked for me when CardImgText was set to files owner for the view.
class CardImgTxt: NSView {
#IBOutlet var view: NSView!
override var frame: NSRect{
didSet{
view.frame = bounds
}
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
}
required init?(coder: NSCoder) {
super.init(coder: coder)
NSBundle.mainBundle().loadNibNamed("View", owner: self, topLevelObjects: nil)
addSubview(view)
}
}
if you are more interested in efficiency than real time updating. Then replace :
override var frame: NSRect{
didSet{
view.frame = bounds
}
}
with:
override func viewDidEndLiveResize() {
view.frame = bounds
}

How to change content view of Window?

I am developing an mac osx application which have initial window and viewcontroller launched from main storyboard. I want to replace the content view loaded by storyboard with my view.
I am doing this -
func replaceContentView() {
parentViewController = MainViewController(nibName: "MainContainerView", bundle: nil)!
let fullScreenFrame = NSScreen.mainScreen()?.visibleFrame
self.initialWindow.setFrame(fullScreenFrame!, display: false, animate: false)
self.initialWindow.contentView = parentViewController! . view
}
Problem with this approach is that the default viewcontroller is never getting deallocated. deinit() of default viewController is not called at all.
This is causing memory leak. So how to completely remove default content view and associated viewcontroller?
Storyboards don't deal in views, they deal in viewcontrollers. What a Storyboard does when it loads a view into a window is that it creates an NSViewController and then goes
windowController.contentViewController = theViewController
That implicitly also inserts theViewController.view as the window's content view. So do the same, and all will be fine.
Marek's example is wrong, because CustomView shouldn't be an NSView subclass, it should be a CustomViewController class that owns a view containing the label etc. As a bonus, NSViewController will take care of loading the XIB for you as well.
Alternately, you could set windowController.contentViewController = nil (which will remove the old view controller and its content view) and then set your content view. But really, why fight the framework when that's exactly what NSViewController is intended for?
You can write the code in deinit method,may it will help you.
deinit {
// perform the deinitialization
}
Your contentViewController within NSWindow instance still holds strongly its old view. You have replaced just property on your NSWindow instance.
To clarify what you have done:
NSWindow holds strongly against new view
NSViewController holds strongly against old view
You should assign your new view into contentViewController.view property as well
This might be helpfull:
NSWindow.h
/* NSViewController Support */
/* The main content view controller for the window. This provides the contentView of the window. Assigning this value will remove the existing contentView and will make the contentViewController.view the main contentView for the window. The default value is nil. The contentViewController only controls the contentView, and not the title of the window. The window title can easily be bound to the contentViewController with the following: [window bind:NSTitleBinding toObject:contentViewController withKeyPath:#"title" options:nil]. Setting the contentViewController will cause the window to resize based on the current size of the contentViewController. Autolayout should be used to restrict the size of the window. The value of the contentViewController is encoded in the NIB. Directly assigning a contentView will clear out the rootViewController.
*/
#availability(OSX, introduced=10.10)
var contentViewController: NSViewController?
/* The view controller for the window's contentView. Tracks the window property of the same name.
*/
#property (strong) NSViewController *contentViewController NS_AVAILABLE_MAC(10_10);
However what you do seems incorrect if you do this on launch.
You either set custom subclass of contentView to your new nsview subclass which can load it's view from another XIB (no need for storyboard).
Abstract example:
class CustomView: NSView {
#IBOutlet var contentView: NSView!
#IBOutlet weak var label: NSTextField!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
func initSubviews() {
let nib = NSNib(nibName: "CustomView", bundle: nil)
nib.instantiateWithOwner(self, topLevelObjects: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
PS: topLevelObjects is set to nil because you hold strongly contentView. So no need to worry about memory management.