Is it possible to create an IBOutlet in a non-view class? - ios

I'm creating a custom presentation controller for dimming the background when a view controller is presented. The presentation controller adds a couple of subviews when the transition begins which works great.
However, I would like to setup the chrome (the presentation "frame") in Interface Builder because that way it's easier to layout. Thus, I created a XIB file for designing the chrome. It includes a semi-transparent background view and a ❌-button in the upper left corner to dismiss the presented view controller. For these subviews I need outlets and actions in my presentation controller (which is not a UIViewController subclass).
In order to achieve that I set the XIB's file's owner to my custom presentation controller, both in Interface Builder and in code when instantiating the view:
lazy var dimmingView = Bundle.main.loadNibNamed("PresentationChromeView",
owner: self,
options: nil)?.first
as! UIView
I then created the respective outlets and actions by CTRL+dragging to my presentation controller:
#IBOutlet var closeButton: UIButton!
#IBAction func closeButtonTapped(_ sender: Any) {
presentingViewController.dismiss(animated: true, completion: nil)
}
However, at run-time the app crashes because UIKit cannot find the outlet keys and when removing the outlets the actions methods are not triggered. So in neither case is the connection established.
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<_SwiftValue 0x600000458810> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key closeButton.'
The only reason I can think of why this doesn't work would be that it's not allowed to create outlets and actions with classes that don't inherit either from UIView or UIViewController.
Is that assumption correct?
Is there a way to create outlets and actions with non-view(-controller) classes?

You can create IBOutlets in any class inheriting from NSObject. The issue here seems to be that you didn't set your custom class in interface builder:
'[<NSObject 0x60800001a940> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key closeButton.'
While decoding your Nib, NSCoder attempts to set the closeButton property on an instance of NSObject, which of course doesn't have this property. You either didn't specify your custom class or made an invalid connection.

OK... the main problem is that the XIB / NIB file has to be instantiated, not just load the first item.
All these changes are in DimmingPresentationController.swift:
// don't load it here...
//lazy var dimmingView = Bundle.main.loadNibNamed("DimmedPresentationView", owner: self, options: nil)?.first as! UIView
var dimmingView: UIView!
then...
private func addAndSetupSubviews() {
guard let presentedView = presentedView else {
return
}
// Create a UINib object for a nib (in the main bundle)
let nib = UINib(nibName: "DimmedPresentationView", bundle: nil)
// Instante the objects in the UINib
let topLevelObjects = nib.instantiate(withOwner: self, options: nil)
// Use the top level objects directly...
guard let v = topLevelObjects[0] as? UIView else {
return
}
dimmingView = v
// add the dimming view - to the presentedView - below any of the presentedView's subviews
presentedView.insertSubview(dimmingView, at: 0)
dimmingView.alpha = 0
dimmingView.frame = presentingViewController.view.bounds
}
That should do it. I can add a branch to your GitHub repo if it doesn't work for you (pretty sure I didn't make any other changes).

The issue that caused the app to crash was that the dimmingView's type could not be inferred (which is strange because the compiler doesn't complain). Adding the explicit type annotation to the property's declaration fixed the crash. So I simply replaced this line
lazy var dimmingView = Bundle.main.loadNibNamed("PresentationChromeView",
owner: self,
options: nil)?.first
as! UIView
with that line:
lazy var dimmingView: UIView = Bundle.main.loadNibNamed("PresentationChromeView",
owner: self,
options: nil)?.first
as! UIView
The outlets and actions are now properly connected.
Why this type inference didn't work or why exactly this fixes the issue is still a mystery to me and I'm still open for explanations. 🙂❓

Related

load xib view in storyboard. why do we have to do self.addSubview(nibView) ?

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view) // My question
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: self.className, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
i want customXibView initial with nib .look like self = loadViewFromNib(), not by self.addSubview(nibView) .
I found that many of the answers are all done self.addSubview(nibView). In fact, look our view hierarchy,we will found a extra view.
This is my view hierarchy
Is there any way to instance xib to storyboard, not by self.addSubview(nibView).
if you don't understand my question,look this Custom UIView from Xib - requirements, good practices. I hope some one help me.
There are three scenarios:
You have CustomView class with all of its UI defined
programmatically in its drawRect method.
You have CustomView class with its UI defined in a subview of storyboard scene with its class set to 'CustomView'.
You have CustomView class with its UI defined in a separate xib.
Now if you want to include your CustomView in as a subview in your viewcontroller, the method differs depending how your class is defined in the first instance. Secondly how do you want to include it in you viewcontroller.
Adding CustomView with scenario 1:
Initialise the CustomView instance in your viewDidLoad method and add it to your viewcontroller's view as a subview.
Adding CustomView with condition 2:
You don't have to do anything in this scenario. Storyboards will initizlize your class for you. You can do additional setup in init(coder) method that will be called when you view is initialized.
Adding CustomView with condition 3:
The proper way to initialise a view with an xib is to initialise it programmatically via UINib's inistantiateWithOwner method. Then add the view as a subview in the viewDidLoad method of your viewcontroller.
What you are doing (or what I can guess seeing your image hierarchy) is that you have changed the class of your UIView to your custom class. Now since you have a separate xib, you are initialising it in init(coder) method and adding it as a subview to the view that was defined in and initialised by the storyboard. Hence you end up with an extra container view.
If you don't want the extra view, add your custom view programmatically.
OR
(If possible) copy the view defined in xib and place it in the viewcontroller dock and then link the view with an outlet and add it in viewDidLoad method.
Here's the answer you've wanted all along. You can just create your CustomView class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.
No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself (i.e. no need for self.addSubview(view), which is your question).
Just do this:
Import BFWControls framework
Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)
That's it!
It even works with IBDesignable to refer your custom view (including the subviews from the xib) at design time in the storyboard.
You can read more about it here:
https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155
And you can get the open source BFWControls framework here:
https://github.com/BareFeetWare/BFWControls
And here's a simple extract of the NibReplaceable code that drives it, in case you're curious:
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
Tom 👣
Its not necessary that you have to add the xib view as subview. Think about the usage, as per my understanding there are tow scenarios
Suppose you have a view called customXibView class and its xib, now in the xib select the view and and from the right side panel select [identity inspector] option and check, under [custom class] by default its class is UIView. So you can keep it as UIView and set the file owner as customXibView and load it from xib as UIView instance as you are doing now. So, now the question is if you allocate customXibView object manually or in some other xib how it will show your custom components which are in your view xib. So in that case in your customXibView's init method you have to load the UIView xib and add it to customXibViewobject's view.
Another case where you can set the xib's view [custom class] as your class customXibView in this case when you call the below method
func loadViewFromNib() -> customXibView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: self.className, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? customXibView
}
You directly get an object of customXibView with UI loaded no need to add it to any other view. But in this case if you allocate the customXibView class object your UI will not be loaded if you don't do what what you are doing now. If any doubt plz comment.

xib with UICollectionView - not key value coding compliant

I have a Custom UIView with an XIB. This custom UIView has a UICollectionView which is connected to an IBOutlet. In the view setup, the UICollectionView is initialised properly and is not nil.
However in the cellForItemAtIndexPath method, I get this error:-
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key selectorCollectionView.'
If I remove the datasource and delegate, I do not get any error. If I add them Iget an error at this line:-
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "selectorCell", for: indexPath) as! SelectorCell
Please help!
Edit: I have attached screenshots of my setup
I have uploaded the project here too http://www.fast-files.com/getfile.aspx?file=148623
Maybe a sync issue. Happens sometimes:
Try cut outlets loose and reconnect them.
Make sure Collection Reusable View identifier is defined in xib file:
Make sure collection-view cell's custom class is define in xib file:
EDIT:
I dug into your project and here are my findings
UICollectionView should be init' in awakeFromNib method (override it):
override func awakeFromNib() {
super.awakeFromNib()
let cellNib = UINib(nibName: String(describing: "SelectorCell"), bundle: nil)
selectorCollectionView.register(cellNib, forCellWithReuseIdentifier: "selectorCell")
selectorCollectionView.dataSource = self
selectorCollectionView.delegate = self
}
loadViewFromNib should be looking like this (Remove collection view init'):
func loadViewFromNib() -> UIView {
let nib = UINib(nibName: "SelectorView", bundle: nil)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
Subclass SelectorTableViewCell likewise.
class SelectorTableViewCell: UITableViewCell {
#IBOutlet weak var selectorView: SelectorView!
}
In Main.storyboard - custom class UITableViewCell to SelectorTableViewCell and connect SelectorView inside contentView to 'SelectorTableViewCell''s outlet.
That's it i think. Here is the project:
You can try the following:
Click on File Owner at the left panel of YourCell.xib. Then go to Attributes inspector and check whether there is any class specified in Class field. If not - no help from me here. If yes - remove it and tap Enter.
In YourCell.xib sequentially click on all subviews and delete all the outlets in Connections Inspector. No need to delete them in YourCell.swift.
Once your cell's File Owner and outlets are removed, we're ready to reconnect the outlets. Just drag every one as usually to an existing corresponding IBOutlet in YourCell.swift.
As a result, when you click on YourCell at the left panel of YourCell.xib and go to Connections Inspector, you'll see there are all the outlets connected. Like so:
And if you check connections for a particular view in YourCell, you should see that it's outlet is connected to YourCell (StaticSlot in my case).
This helped me and hope you'll get the same.
In our case, the problem was making the wrong connection in Storyboard. Click on the custom class in Storyboard -- not the File Owner -- then connect the IB outlet to the variable in the custom class.
if try to register you custom class and cell ID
self.collectionView.register(nib, forCellWithReuseIdentifier: "Cell")
self.collectionView.register(CustomClassCell.self, forCellWithReuseIdentifier: "Cell")

How to get button click from UIView to ViewController?

I have created on custom View which contain one label and button
I have created outlet for both in UIView class
I have one UIViewController in which i have subview that custom view
now i want to change label name and want to do something on button click.
let commonView = UINib(nibName: "CommonView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! CommonView
firstView.addSubview(commonView)
commonView.lblCommon.text = "We can change label in particular Screen"
I am able to change label text but how can i get button action event in UIViewcontroller?
Use protocol
In your customView declare a protocol
protocol CustomViewProtocol : NSObjectProtocol{
func buttonTapped()
}
Now create a variable in custom view
weak var delegate : CustomViewProtocol? = nil
In your viewController confirm to protocol
extension ViewController : CustomViewProtocol {
func buttonTapped() {
//do whatever u waana do here
}
}
set self as delegate in view controller
let commonView = UINib(nibName: "CommonView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! CommonView
firstView.addSubview(commonView)
commonView.delegate = self
finally in IBAction of button in custom view trigger delegate
self.delegate?.buttonTapped()
In Swift 4:
If you want to catch the tap within your UIViewController do this in the viewDidLoad() after you instantiate your view from a nib (eg called aView):
let tap = UITapGestureRecognizer.init(target: self, action: #selector(tapAction))
aView.addGestureRecognizer(tap)
Below viewDidLoad declare the following function and do whatever you want within its body:
#objc func tapAction() {
//Some action here
}
(A note: In Swift 4 the #objc annotation is obligatory, because it denotes that you want the function to be visible to the Obj-C runtime, which in UIKit handles (among other things) user interactions through its messaging system.)
Since this UIView instance belongs in the view hierarchy of this UIViewController it makes sense to handle the taps within this UIViewController. This gives you the ability to reuse this UIView within any other UIViewController and handle its tap logic from the respective UIViewController.
In case there is a particular reason you want the taps handled within the UIView, you can use the delegate pattern, a closure, or in a more advanced implementation a Reactive framework (ReactiveSwift or RxSwift/RxCocoa)
The way to do it with RxSwift/RxCocoa:
view.rx
.tapGesture()
.when(.recognized)
.subscribe(onNext: { _ in
//react to taps
})
.disposed(by: stepBag)
more ways to manage Gestures, can be found at the RxSwift github page: https://github.com/RxSwiftCommunity/RxGesture

Opening a UIView from another UIView

I have a UIViewController with a button that opens a Modal/Popup (XIB)
I use this code for that:
let aView: myView1 = UINib(nibname: "AView", bundle : nil).instantitate(withOwner: self, options: nil)[0] as! myView1
aView.frame = self.view.frame
self.view.addSubView(aView)
The Modal contains a button that should open a Form (another XIB)
I use this code for that:
let bView: myView2 = UINib(nibname: "BView", bundle : nil).instantitate(withOwner: self, options: nil)[0] as! myView2
bView.frame = self.view.frame
self.view.addSubView(bView)
But I get a compile time error, saying:
Value of type 'AView' has no member 'view'
When I changed the code from:
self.view.addSubview(bView)
to:
self.addSubview(bView)
I get a runtime error when using above line in UIView:
this class is not key value coding-compliant for the key view.
If I relocate the above line to UIViewController, I get a different error:
Can't add self as subview
Is there some other way that works to open a UIView from another UIView, without going back or making changes to the UIViewController?
First i suggest change class name from myView1 to MyView1, same to myView2. Then set your nib files file's owner from interface builder to MyView1 and MyView1.
Change this withOwner: self to nil in below call:
let aView:myView1 = UINib(nibname: "AView", bundle : nil).instantitate(withOwner: self, options: nil)[0] as! myView1
OR
You might need to remove file's owner from interface builder.

View is loaded from nib but is not visible?

I create two custom views using nib files and I create one instance for each class and then I added the two views to my main view , the first view is added and the second is loaded but not visible.
I load the views from the nib file using the following class method
class func instanceFromNib() -> CustomView {
let view = NSBundle.mainBundle().loadNibNamed("CustomView", owner: self, options: nil).first as! CustomView
return view
}
I need help to figure out what is going wrong with the second view.
try this:
view.translatesAutoresizingMaskIntoConstraints = true
view.autoresizingMask = .None

Resources