I've created a custom UIView with multiple IBOutlets including a UIImageView, and three UILabels. Despite setting the image value in the awakeFromNib() function, I'm still getting a nil value for the outlet when attempting to set it. The UIView was constructed using a custom xib file.
I put the set operation in awakeFromNib() so as to assure the outlets had been initialized prior to setting them, yet this failed to help. I'm not sure if it's an issue with the xib file as I've only ever used custom xibs when making a custom table cell, so perhaps the issue is rooted there?
class VotingCard: UIView {
#IBOutlet weak var proPicImg: UIImageView!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var hometownLabel: UILabel!
#IBOutlet weak var ratingLabel: UILabel!
var proPic = UIImage()
var name = ""
var hometown = ""
var rating = 0.0
init(pic: UIImage, rush: Rush) {
proPic = pic
name = rush.fullName
hometown = rush.homeTown
rating = rush.compositeRating
super.init(frame: CGRect(x: 0, y: 0, width: 250, height: 300))
}
override func awakeFromNib() {
proPicImg.image = proPic
nameLabel.text = name
hometownLabel.text = hometown
ratingLabel.text = "\(rating)"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
I should view a simple card with the profile image and respective data visible, yet instead it crashes.
Your problem is either
1-The imageView isn't connected to Ib , and the fix is to connect it
Or
2- You create an object of the view with init(pic: UIImage, rush: Rush) { and that for sure won't load the UI with view , hence all outlets are nil , and the fix to add this method
class func getInstance(_ pic: UIImage, rush: Rush) -> VotingCard {
let v = Bundle.main.loadNibNamed("VotingCard", owner: self, options: nil)!.first as! VotingCard
v.proPic = pic
v.name = rush.fullName
v.hometown = rush.homeTown
v.rating = rush.compositeRating
return v
}
Call
let v = VotingCard.getInstance(<#image#>,rush:<#rush#>)
v.frame = // set some frame
// ready for use
EDIT
NIB-based development is sort-of deprecated:
For iOS developers, using storyboards is the recommended way to design
user interfaces.
https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/NibFile.html
But, looks like you're missing this step:
At runtime, you load a nib file using the method loadNibNamed:owner:
or a variant thereof. The File’s Owner is a placeholder in the nib
file for the object that you pass as the owner parameter of that
method. Whatever connections you establish to and from the File’s
Owner in the nib file in Interface Builder are reestablished when you
load the file at runtime.
See as an example:
https://medium.com/#umairhassanbaig/ios-swift-creating-a-custom-view-with-xib-ace878cd41c5
If you want to do it the recommended way with Storyboard (apparently not, but this is for general audience):
https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/WorkWithViewControllers.html
viewDidLoad()—Called when the view controller’s content view (the top
of its view hierarchy) is created and loaded from a storyboard. The
view controller’s outlets are guaranteed to have valid values by the
time this method is called. Use this method to perform any additional
setup required by your view controller.
So with the UIViewController method, put your code in viewDidLoad() instead.
Keep in mind this is always called, so this will overwrite the work of your custom init() function. Perhaps this is what you were running into.
I would refactor it to avoid ugly code. Create the Rush object always (declare as Optional as stored property), but initialize it in awakeFromNib() for the XIB case, and copy the input object in the custom-init case.
Then viewDidLoad() has no conditionals.
Related
I have a custom UIView class that is initiated from the xib file. It has instance property called title of type String?. Whenever, the title property is set, the text of a UITextField gets changed to the value of the title property.
If the title property is a stored property, the program works as expected.
If the title property is a computed property, then the program crashes with EXC_BAD_ACCESS error which I assume is because an IBOutlet had not yet been initialized.
Can anyone explain why if title is a stored property, it works but if it is an computed property it fails?
Following is the source code-
The NibView is a subclass of UIView and handles the loading of xib file
class NibView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
loadNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadNib()
}
}
The implementation of loadNib method is inside an extension
extension UIView {
func loadNib() {
guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { return }
view.frame = bounds
addSubview(view)
}
}
The definition of nib property on UIView is in another extension
extension UIView {
static var nib: UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
var nib: UINib {
return type(of: self).nib
}
}
The following class is the class which has the title property.
class ProgressView: NibView {
var title: String? {
didSet {
titleLabel.text = title
}
}
#IBOutlet private weak var titleLabel: UILabel!
}
The above class is used as follows-
let view = ProgressView()
addSubview(view)
view.title = "Loading"
Running the above code works as expected.
However if the implementation of ProgressView is changed to use a computed property as below, then it fails
class ProgressView: NibView {
var title: String? {
get {
return titleLabel.text
}
set {
titleLabel.text = newValue
}
}
#IBOutlet private weak var titleLabel: UILabel!
}
Can anyone point out why where is difference in behaviour when the title property is computed instead of being stored?
Edit -
The main thread crashes with "Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)"
The method on top of the call stack is "ProgressView.title.modify".
Edit 2-
I am not sure what I have done but I am unable to reproduce the issue after restarting xcode. Even if computed property is used, it works as expected.
Your description is far from explanatory, but I'm guessing that there is a ProgressView nib in which the File's Owner is a ProgressView, and there is an titleLabel outlet from the File's owner to a label inside the nib. (I assume this because otherwise I can't explain your use of withOwner: self.)
On that assumption I can't reproduce any problem. Both your ways of expressing title work just fine for me. I put print statements to make sure the right one was being called, and it is; no matter whether this is a didSet or the setter of a computed property, we load just fine and I see the "Loading" text.
My code is in a view controller's viewDidLoad, if that makes a difference.
(By the way, I regard your use of ProgressView() with suspicion. This results in a zero-size view. It might not seem to make any difference, but it's a bad idea. The label is a subview of the zero-size view. If the zero-size view clipped its subviews, the label would be invisible. Even if the zero-size view does not clip its subviews, if the label were a button, the button would not work. Zero-size views are a bad idea. You should give your ProgressView a real frame.)
I'm attempting to use a custom uiview for a tinder-style card swipe library called Koloda, which requires that I pass in a uiview for each card in the stack. I'm attempting to use a custom uiview with relevant fields/info, and after thorough testing I've discovered that generating an instance of uinib is giving me a 'not key value coding-compliant' error, despite having all my outlet connections in order.
I've tried recreating all my IBOutlets, going as far as to just start over with a new xib file with new connections. When I drag a uiview onto a view controller in storyboard, and conform it to my custom type, it shows up no problem and I can manipulate its various properties as desired. It's only when instantiating the custom view that I get the error.
class VoteCard: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var proPicImg: UIImageView!
#IBOutlet weak var nameLbl: UILabel!
#IBOutlet weak var homeLbl: UILabel!
#IBOutlet weak var ratingLbl: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed("VoteCard", owner: self, options: nil)
contentView.fixInView(self)
}
class func instanceOf() -> VoteCard {
return UINib(nibName: "VoteCard", bundle: nil).instantiate(withOwner: nil, options: nil).first! as! VoteCard
}
}
If all works correctly, my Koloda card stack should show my custom cards, however as of yet I can't get passed the generation of a new instance.
Please see this answer: https://stackoverflow.com/a/52831749/2537616
Here is the idea:
Set the BaseClass of your custom view inside .xib file.
Connect outlet as usual.
Then create static helper to instantiate UINib.
There might be some outlets which was define first and then removed from your class. But they are not removed from your storyboard or xib. Please check for those outlets in your view and remove them. Please refer the image. Here buttonShow is not in my view controller so remove it.
Hope it helps.
I am new to iOS development so forgive me if I'm missing something obvious. I have a view controller that contains a subview in which I've created a numpad, and for the time being I want to give the numpad view its own UIView subclass because I want to do a few different things with it. Right now the numpad is just creating a string from the keys that get pressed, and I've set up a delegate to pass that string anywhere else I want to use it (though I've also tried accessing the raw input directly in the view controller with let a = subview(); label.text = a.rawInput).
Whenever I try to set the text of the UILabel in the view controller to the subview's raw input, whether by delegation or directly, the UILabel is found to be nil and throws the error in the title.
Things I've tried:
Setting the text inside a viewDidLoad override, and outside of it
Setting a variable (testInput) inside the view controller to adopt the subview's raw input and setting the label text to that (I've confirmed that the variable inside the view controller gets properly set, so no delegation issues)
Using didSet on the testInput variable both to set label text to testInput and to try calling viewDidLoad and set the label text in there (printing testInput inside this didSet does print the right string, FWIW)
Deleting and relinking the IBOutlet for my label
Strong and weak storage for the IBOutlet variable
Trying to do the same thing in another subview within the view controller, in case for some reason it was the view controller's own fault
Searching everywhere for a solution that works
I'm stumped. Here is my relevant numpad code:
import UIKit
protocol NumpadDelegate {
func updateInput(input: String)
}
class Numpad: UIView {
// MARK: UI outlets
#IBOutlet weak var decButton: UIButton!
// MARK: Properties
var rawInput: String = ""
var visibleInput: String = ""
var calcInput: String = ""
var operandReady = 1
var percentWatcher = 0
var delegate: NumpadDelegate? = BudgetViewController()
// MARK: Functions
func handleRawInput(str: String) {
rawInput += str
print("numpad input is \(rawInput)")
delegate?.updateInput(rawInput)
}
And here is the view controller code:
import UIKit
class BudgetViewController: UIViewController, NumpadDelegate {
// MARK: Properties
//#IBOutlet weak var transactionValueField: UITextField!
#IBOutlet weak var remainingCashForIntervalLabel: UILabel!
#IBOutlet weak var intervalDenoterLabel: UILabel!
#IBOutlet weak var currencyDenoterLabel: UILabel!
#IBOutlet weak var mainDisplayView: TransactionType!
#IBOutlet weak var inactiveInputView: InactiveInput!
#IBOutlet weak var numpadView: Numpad!
#IBOutlet weak var rawInputLabel: UILabel!
var remainingCashForInterval = 40
let display = TransactionType()
var testInput = "" {
didSet {
viewDidLoad()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// let numpad = Numpad()
// numpad.delegate = self
// print("\(numpad.delegate)")
self.rawInputLabel.text = testInput
}
func updateInput(input: String) {
print("view controller now has \(input)")
display.mainInput = input
testInput = input
}
As a side note, in case you noticed that my protocol isn't a class type, for some reason adding : class to it and declaring my delegate as a weak variable prevents the delegation from working. Any suggestions there?
You assigned the delegate like so:
var delegate: NumpadDelegate? = BudgetViewController()
That doesn't reference the view controller whose scene was presented, but rather a new blank one. And that's why when you used weak, why it was deallocated (because that orphaned instance of the view controller has no strong references to it).
You should define the protocol to be a class protocol again, and define delegate to be:
weak var delegate: NumpadDelegate?
And then, in the view controller's viewDidLoad, uncomment the line that sets that delegate:
numpadView.delegate = self
But, do not uncomment the line that says numpad = Numpad(); that is incorrect as that creates yet another Numpad instance. But you do want to set the delegate of the existing Numpad, though.
Both of these issues (namely, getting a reference to the view controller that is to be the delegate of the Numpad view; and getting a reference to the Numpad view that the storyboard presented) suggest some misunderstanding about the the process of presenting a storyboard scene.
The process is basically as follows:
the view controller is instantiated, using whatever class you specified as the base class for that scene;
its root view, as well as all of the subviews on that scene will be instantiated for you;
the storyboard will hook up the IBOutlet references in the scene's base class to the outlets you created; and
the view controller's viewDidLoad is called.
That's an oversimplification, but that's the basic process.
But the key is that all of these view controllers and views that are referenced on the storyboard scene are created for you. You don't want to try to create any of these yourself (and the presence of the () at the end of BudgetViewController() or Numpad() says "create a new instance of x", which is not what we want to do here).
So, when we need to get a reference to the view controller so that we can programmatically specify the delegate for one of the views, you can do this in viewDidLoad, at which point self references the view controller that the storyboard instantiated for us. We don't want to instantiate a new one. Likewise, when you want to reference the Numpad that the storyboard instantiated for us (in order to hook up its delegate), you use the IBOutlet you hooked up in Interface Builder, rather than programmatically instantiate a new Numpad with Numpad().
I'm using Swift and Xcode 6.4 for what it's worth.
So I have a view controller that will be containing some multiple pairs of UILabels and UIImageViews. I wanted to put the UILabel-UIImageView pair into a custom UIView, so I could simply reuse the same structure repeatedly within the aforementioned view controller. (I'm aware this could be translated into a UITableView, but for the sake of ~learning~ please bear with me). This is turning out to be a more convoluted process than I imagined it would be, I'm having trouble figuring out the "right" way to make this all work in IB.
Currently I've been floundering around with a UIView subclass and corresponding XIB, overriding init(frame:) and init(coder), loading the view from the nib and adding it as a subview. This is what I've seen/read around the internet so far. (This is approximately it: http://iphonedev.tv/blog/2014/12/15/create-an-ibdesignable-uiview-subclass-with-code-from-an-xib-file-in-xcode-6).
This gave me the problem of causing an infinite loop between init(coder) and loading the nib from the bundle. Strangely none of these articles or previous answers on stack overflow mention this!
Ok so I put a check in init(coder) to see if the subview had already been added. That "solved" that, seemingly. However I started running into an issue with my custom view outlets being nil by the time I try to assign values to them.
I made a didSet and added a breakpoint to take a look...they are definitely being set at one point, but by the time I try to, say, modify the textColor of a label, that label is nil.
I'm kind of tearing my hair out here.
Reusable components seem like software design 101, but I feel like Apple is conspiring against me. Should I be looking to use container VCs here? Should I just be nesting views and having a stupidly huge amount of outlets in my main VC? Why is this so convoluted? Why do everyone's examples NOT work for me?
Desired result (pretend the whole thing is the VC, the boxes are the custom uiviews I want):
Thanks for reading.
Following is my custom UIView subclass. In my main storyboard, I have UIViews with the subclass set as their class.
class StageCardView: UIView {
#IBOutlet weak private var stageLabel: UILabel! {
didSet {
NSLog("I will murder you %#", stageLabel)
}
}
#IBOutlet weak private var stageImage: UIImageView!
var stageName : String? {
didSet {
self.stageLabel.text = stageName
}
}
var imageName : String? {
didSet {
self.stageImage.image = UIImage(named: imageName!)
}
}
var textColor : UIColor? {
didSet {
self.stageLabel.textColor = textColor
}
}
var splatColor : UIColor? {
didSet {
let splatImage = UIImage(named: "backsplat")?.tintedImageWithColor(splatColor!)
self.backgroundColor = UIColor(patternImage: splatImage!)
}
}
// MARK: init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if self.subviews.count == 0 {
setup()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
if let view = NSBundle.mainBundle().loadNibNamed("StageCardView", owner: self, options: nil).first as? StageCardView {
view.frame = bounds
view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
addSubview(view)
}
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
}
EDIT: Here's what I've been able to get so far...
XIB:
Result:
Problem: When trying to access label or image outlets, they are nil. When checking at breakpoint of said access, the label and image subviews are there and the view hierarchy is as expected.
I'm OK with doing this all in code if thats what it takes, but I'm not huge into doing Autolayout in code so I'd rather not if there's a way to avoid it!
EDIT/QUESTION SHIFT:
I figured out how to make the outlets stop being nil.
Inspiration from this SO answer: Loaded nib but the view outlet was not set - new to InterfaceBuilder except instead of assigning the view outlet I assigned the individual component outlets.
Now this was at the point where I was just flinging shit at a wall and seeing if it'd stick. Does anyone know why I had to do this? What sort of dark magic is this?
General advice on view re-use
You're right, re-usable and composable elements is software 101. Interface Builder is not very good at it.
Specifically, xibs and storyboard are great ways to define views by re-using views that are defined in code. But they are not very good for defining views that you yourself wish to re-use within xibs and storyboards. (It can be done, but it is an advanced exercise.)
So, here's a rule of thumb. If you are defining a view that you want to re-use from code, then define it however you wish. But if you are defining a view that you want to be able to re-use possibly from within a storyboard, then define that view in code.
So in your case, if you're trying to define a custom view which you want to re-use from a storyboard, I'd do it in code. If you are dead set on defining your view via a xib, then I'd define a view in code and in its initializer have it initialize your xib-defined view and configure that as a subview.
Advice in this case
Here's roughly how you'd define your view in code:
class StageCardView: UIView {
var stageLabel = UILabel(frame:CGRectZero)
var stageImage = UIImageView(frame:CGRectZero)
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
required init(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)
setup()
}
private func setup() {
stageImage.image = UIImage(named:"backsplat")
self.addSubview(stageLabel)
self.addSubview(stageImage)
// configure the initial layout of your subviews here.
}
}
You can now instantiate this in code and or via a storyboard, although you won't get a live preview in Interface Builder as is.
And alternatively, here's roughly how you might define a re-usable view based fundamentally on a xib, by embedding the xib-defined view in a code-defined view:
class StageCardView: UIView {
var embeddedView:EmbeddedView!
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
required init(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)
setup()
}
private func setup() {
self.embeddedView = NSBundle.mainBundle().loadNibNamed("EmbeddedView",owner:self,options:nil).lastObject as! UIView
self.addSubview(self.embeddedView)
self.embeddedView.frame = self.bounds
self.embeddedView.autoresizingMask = .FlexibleHeight | .FlexibleWidth
}
}
Now you can use the code-defined view from storyboards or from code, and it will load its nib-defined subview (and there's still no live preview in IB).
I was able to work it around but the solution is a little bit tricky. It's up to debate if the gain is worth an effort but here is how I implemented it purely in interface builder
First I defined a custom UIView subclass named P2View
#IBDesignable class P2View: UIView
{
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var iconView: UIImageView!
#IBInspectable var title: String? {
didSet {
if titleLabel != nil {
titleLabel.text = title
}
}
}
#IBInspectable var image: UIImage? {
didSet {
if iconView != nil {
iconView.image = image
}
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
awakeFromNib()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override func awakeFromNib()
{
super.awakeFromNib()
let bundle = Bundle(for: type(of: self))
guard let view = bundle.loadNibNamed("P2View", owner: self, options: nil)?.first as? UIView else {
return
}
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
let bindings = ["view": view]
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"V:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"H:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
addConstraints(verticalConstraints)
addConstraints(horizontalConstraints)
}
titleLabel.text = title
iconView.image = image
}
This is how it looks like in interface builder
This is how I embedded this custom view in the example view controller defined on a storyboard. Properties of P2View are set in the attributes inspector.
There are 3 points worth mentioning
First:
Use the Bundle(for: type(of: self)) when loading the nib. This is because the interface builder renders the designables in the separate process which main bundle is not the same as your main bundle.
Second:
#IBInspectable var title: String? {
didSet {
if titleLabel != nil {
titleLabel.text = title
}
}
}
When combining IBInspectables with IBOutlets you have to remember that the didSet functions are called before awakeFromNib method. Because of that, the outlets are not initialized and your app will probably crash at this point. Unfortunatelly you cannot omit the didSet function because the interface builder won't render your custom view so we have to leave this dirty if here.
Third:
titleLabel.text = title
iconView.image = image
We have to somehow initialize our controls. We were not able to do it when didSet function was called so we have to use the value stored in the IBInspectable properties and initialize them at the end of the awakeFromNib method.
This is how you can implement a custom view on a Xib, embed it on a storyboard, configure it on a storyboard, have it rendered and have a non-crashing app. It requires a hack, but it's possible.
Maybe it is the wrong idea completely to use the same xib multiple times in one viewcontroller, but it looked in some way better than creating an x amount of views with the same labels and the same buttons..
And of course some fun with xibs:)
To give you a quick impression of what I achieved so far:
In the class belonging to the xib, I created a delegate so I could catch a button press in my main view controller.
import UIKit
protocol TimerViewDelegate : class {
func timerButtonTapped(buttonState : NSInteger)
}
class TimerView: UIView {
var delegate : TimerViewDelegate?
var currentButtonState = 0
#IBOutlet var view: UIView!
#IBOutlet var timerLabel: UILabel!
#IBOutlet var button: UIButton!
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
NSBundle.mainBundle().loadNibNamed("TimerView", owner: self, options: nil)
self.addSubview(self.view)
}
#IBAction func buttonTapped(sender:UIButton) {
if currentButtonState == 0{
currentButtonState = 1
} else{
currentButtonState = 0
}
self.delegate?.timerButtonTapped(currentButtonState)
}
}
I know it is not super fancy stuff, but at the moment I'm only evaluating if its any use to do this at all.
In my main view controller I registered outlets for the xibs in a way like:
#IBOutlet weak var timerView1 : TimerView!
#IBOutlet weak var timerView2 : TimerView!
#IBOutlet ...
And in viewDidLoad():
self.timerView1.delegate = self
self.timerView2.delegate = self
self...
Later I can catch the button presses in the following method:
func timerButtonTapped(buttonState: NSInteger) {
println("button press from nib, with state \(buttonState)")
}
From here it does make a difference if I press the button from the top xib or another one, since they keep track of their own buttonstate.
But how can I distinguish the different xibs from each other like this?
I can give the xibs themselves a tag, but I don't know if that has any use. Also talking to their labels from my main view, will have a simular problem..
Even if this is a completely wrong approach of using xibs, I'm still interested how to solve this.
Thank you for your time!
You pretty much have your solution already, you just need to improve your protocol method specification, basically by adding the TimerView that is passing on the button press.
(compare to a delegate protocol like UITableViewDelegate, where the table view always passes itself...)
So, something like:
protocol TimerViewDelegate : class {
func timerButtonTapped(sender: TimerView, buttonState : NSInteger)
}
and then the delegate can find out which TimerView is associated and do something with it.
Incidentally, it's likely best to store the TimerView instances in an array, sorted in some way, so that you can easily access the details.
You can use the tag property safely. Apple documentation says:
An integer that you can use to identify view objects in your application.