How to access view from XIB-Instance - ios

I have an .xib file with a few different view elements. Now I make several instances of it in my ViewController with this:
UINib(nibName: "\(self)", bundle: nil).instantiate(withOwner: nil, options: nil).first
Now I am not sure, how to access my viewElements inside this UIView
I could do myView.subviews() and iterate over them. But is there any way to get them by their label, so that I can directly change the text of a UITextField from my xib for example?

You've got a few options.
The first one would be to place all the views in your xib that are not related at top level, then you can access them by their index. For example, if you have a UIView, a UITextField and a UILabel, in that order, you can get the text field like this:
let textField = UINib(nibName: "\(self)", bundle: nil).instantiate(withOwner: nil, options: nil)[1] as? UITextField
The second option, if you want to keep your views as subviews, would be to create a container class with IBOutlets for it's views, set it as the class of the root view, and connect the outlets, then you'd get the text field like this
let textField = (UINib(nibName: "\(self)", bundle: nil).instantiate(withOwner: nil, options: nil).first as? ContainerClass)?.textFIeld
Instead of a container class, you could also access them by the index of the subview.
let textField = (UINib(nibName: "\(self)", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView)?.subviews[1] as? TextField
Another option is to use generics, this solution is clever an Swifty but will only work if you only have one view of the given class. If you have to text fields, it will return the first one.
func findFirstElement<T>(of kind: T.Type, in array: [Any]) -> T? {
for element in array {
if let found = element as? T {
return found
}
}
return nil
}
You get the idea. You can use it as it is, as an extension of UINib that goes through it's contents, or as an extension of UIView that finds the subview that matches.
The cool thing is that the return value of that object is already of the type you expect, the downside is that it'll only find the first one.
Personally, I would use the first option. If the views are unrelated, I wouldn't add them as subviews of another.

Related

Should we prefer to use Bundle.main or UINib when creating custom view from XIB?

There are 2 different ways, to create custom view from XIB
Bundle.main
extension UIView {
static func instanceFromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: self), owner: nil, options: nil)![0] as! T
}
}
UINib
extension UIView {
static func instanceFromNib<T: UIView>() -> T {
return UINib(nibName: String(describing: self), bundle: nil).instantiate(withOwner: self, options: nil)[0] as! T
}
}
I was wondering, what is the differences among the 2?
Which one is a better way, or it really doesn't matter?
There isn't any difference. The former is what I generally use, but just because it's the older version and so what I'm used slightly more used to from before Storyboards. UINib wasn't added until iOS 4. Before that, there wasn't any way to represent the NIB itself, and you couldn't load a NIB from data (NSData/Data). Adding that allows for some tricks when you don't want to cache the NIB, or when you want to load a NIB dynamically (not from the Bundle). But these are really obscure corner cases.
The Bundle version is just a little shorter usually, and what I recommend. But whichever you like. They're equivalent.

UIViewController xib file containing a collectionview with a custom cell

I have a xib file containing an UIViewController called FullScreenGalleryVC. It contains a UICollectionView that contains a custom UICollectionViewCell, which identifier is fullscreen_cell.
When I want to create a new FullScreenGalleryVC, I call FullscreenGalleryVC.display(from: self).
Here's the func:
class func display(from sourcevc:UIViewController){
let vc=UINib(nibName: "FullscreenGalleryVC", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! FullscreenGalleryVC
sourcevc.present(vc, animated: true, completion: nil)
}
I get this error:
could not dequeue a view of kind: UICollectionElementKindCell with
identifier fullscreen_cell - must register a nib or a class for the
identifier or connect a prototype cell in a storyboard
It works fine if I put the custom cell in another xib file and call register(nibClass, forCellWithReuseIdentifier: cellId).
It works fine if I don't put this FullScreenGalleryVC class in a separate xib file (and keep it in my main storyboard).
But I use this class from both the app and an action extension so that's why I'd like to use a common file instead of duplicating everything. Is there a way to do that or do I imperatively have to put the custom cell in a different xib file to make it work?
If you a xib file for FullScreenGalleryVC controller then you should use register(nibClass, forCellWithReuseIdentifier: cellId) to tell the collection view how to create a new cell of the given type.
let cellNib = UINib(nibName: "FullScreenGalleryCell", bundle: .main)
collectionView.register(cellNib, forCellWithReuseIdentifier: "fullscreen_cell")
If you use a storyboard, then it's already known to the collection view with the specified cellId.
In my opinion, you can use a storyboard for FullScreenGalleryVC and reuse the same for other classes.
Present a viewController from a specific storyboard like,
let storyboard = UIStoryboard(name: "YourStoryboardName", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "storyboardId")
self.present(controller, animated: true, completion: nil)

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.

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

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. 🙂❓

Resources