Best strategy to load custom UIView with XIB and Outlets - ios

What is best strategy to load custom UIViews with XIB and Outlets? At this moment I have code listed below. I think this code is bad because I have 2 UIViews as container and in future probably problem with constraints.
UIViewController ( I don't want all outlets and actions in one big ViewController )
func showCategories() {
if(self.categoriesView == nil) {
self.categoriesView = CategoriesView()
}
self.view.addSubview(self.categoriesView!)
}
Custom UIView - CategoriesView
class CategoriesView, ...protocols... {
#IBOutlet var table:UITableView!
override init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame:CGRect) {
super.init(frame: frame)
}
override init() {
super.init()
let views = NSBundle.mainBundle().loadNibNamed("CategoriesView", owner: self, options: nil)
let view = views![0] as CategoriesView
self.frame = view.frame
self.addSubview(view)
}
....
}

In Apple's MVC, it's best to avoid views with too much logic in them. If you want to compose a complex view using component subviews, then look at Creating Custom Container View Controllers.
If you are already using storyboards, a container view will take care of most of the complexity for your.

Related

Adding A UIGestureRecognizer To A custom .xib View

I want to add a tab recognizer to a custom .xib file view in my ios swift app. Here's the code from the owner class of the .xib file:
import UIKit
class WordLabel: UILabel {
#IBOutlet weak var wordLabel: UILabel!
#IBOutlet var wordFrame: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
//I added the tab recognizer here
wordLabel.isUserInteractionEnabled = true
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(labelClicked(_:)))
wordLabel.addGestureRecognizer(gestureRecognizer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed(K.wordLabelNibName, owner: self, options: nil)
addSubview(wordFrame)
wordFrame.frame = self.bounds
wordFrame.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
//should happen when label is tapped
#objc func labelClicked(_ sender: Any) {
print("UILabel clicked")
}
}
When I ran the project on a phone simulator, there were no errors.
App Running On The Simulator
But when I clicked the labels that showed up, the message was not printed onto the console (meaning that the action was not triggered). What am I doing wrong?
Thank you for your help.
github link to my project:
https://github.com/ShaungYi/PolyLibrum/blob/main/PolyLibrum/View/BookReader/WordLabel/WordLabel.swift
When you use WordLabel custom UILabel with xib, init(frame:) constructor is not called. init(coder:) is called instead. Because of that, your gesture recognizer doesn't work. You must to move the gesture recognizer assignment to commonInit() method.
I solved the problem on my own. It seems that I set the owner class of the .xib file to inherit from a UILabel. When I changed the superclass to the generic UIView, everything worked out perfectly! I don't really understand how this fixed the problem, but my theory is that previously, the owner class relegated the UIGesture event to the UILabel class instead of itself, thus not triggering the bound gesture recognizer. Now That I diverted that to the owner class, there's no such problem.
So for anyone else who has my problem- set the owner class' superclass to the generic UIView if possible?
Thanks again to Furkan Kaplan for his help.

Replacing UIView's self with another instance in Swift

I want to initialize xib-files programmatically in Swift, so I created the class MyView.
The initialization of the xib is declared in the setup() method, where loadNibNamed() is called. This returns an additional view, which I have to add as a subview to my current/initial view.
I saw in User Interface Inspector that behind MyView is the initial view, which has of course also own properties. I do not like this behaviour and do not want to modify properties twice. In the end I want to achieve that the instance from the initializer would be replaceable with the instance that has been created by the call of loadNibNamed(); figurative something like self = view.
I added the code of the initializers and the setup() method.
required init?(coder aDecoder: NSCoder) {
NSLog("init with NSCoder")
super.init(coder: aDecoder)
}
init(in frame: CGRect) {
NSLog("init with CGRect")
super.init(frame: frame)
setup()
}
private func setup() {
NSLog("setting up")
let view = Bundle.main.loadNibNamed("MyView", owner: self, options: nil)!.first as! MyView
addSubview(view)
}
You cannot substitute one self for another in an initializer. init and nib-loading are related, but the relationship runs the opposite way from your proposal: loading the view from the nib will call your init(coder:).
What you need is not an initializer but a factory.
Give MyView a class method (class func) that a client can call to load the nib and return the instance.
class func new() -> MyView {
let view = Bundle.main.loadNibNamed("MyView", owner: nil, options: nil)!.first as! MyView
return view
}
Usage:
let v = MyView.new()

Is this when I should be creating a class?

I'm trying to get to grip with Swift and wanted some advice...
I have a UIView that exists on a number of screens; specifically, it's a logo that uses a number of elements/parameters to style it correctly i.e. shadow, shape, image etc.
For my first viewcontroller I set this up as a function that is called from the viewDidLoad function. Then in my second viewcontroller I have the same logo... here is my question,
Should I load the first view controller from the story board and then reference the function in the second viewcontroller OR should I have just made the logo a class that either viewcontroller can reference? My gut says it should be a class...
Thanks in advance
For a reusable view you’d create a XIB in which you design the view plus a view controller class in which you instantiate the xib, like this:
class ReusableView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "ReusableView", bundle: bundle)
if let view = nib.instantiate(withOwner: self, options: nil).first as? UIView {
view.frame = bounds
addSubview(view)
}
}
}
In your view controller(s), you’d then simply place a placeholder UIView at the desired location and set its custom type to ReusableView. By connecting an outlet from this view into your view controller you’d have access to the view’s properties.
Please note that you will have to leave the Custom View property in the XIB set as UIView and set File’s Owner to ReusableView instead. Otherwise you’ll create an infinite loop.
WARNING! As I pointed out on another answer, DO NOT load the same nib from within awakeFromNib, or else you'll create an infinite loop of loading nibs.
Your gut instinct is correct, I would say. Create a custom class for your reusable view to go in. If you decide to create a nib for your class, I recommend instantiating it from a static function.
class ReusableView: UIView {
static func newFromNib() -> ReusableView {
let bundle = Bundle(for: ReusableView.self)
let nib = UINib(nibName: "ReusableView", bundle: bundle)
guard let view = nib.instantiate(withOwner: self, options: nil).first as? ReusableView else {
preconditionFailure("Could not instantiate ReusableView")
}
return view
}
override func awakeFromNib() {
super.awakeFromNib()
// Configuration ONLY if you use a Nib.
}
override init(frame: CGRect) {
super.init(frame: frame)
// Configuration if you DO NOT use a nib
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Then, you can use it in a view controller:
class ViewController: UIViewController {
#IBOutlet weak var reusableViewContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Create a new instance of your reusable view
let reusableView = ReusableView.newFromNib()
// If not using a nib, just use the 'init(frame:)' method
// let reusableView = ReusableView(frame: .zero)
// Add your reusable view to the view hierarchy
self.reusableViewContainer.addSubview(reusableView)
// Layout your view as necessary:
// For example, if using AutoLayout:
self.reusableViewContainer.topAnchor.constraint(equalTo: reusableView.topAnchor).isActive = true
self.reusableViewContainer.bottomAnchor.constraint(equalTo: reusableView.bottomAnchor).isActive = true
self.reusableViewContainer.leadingAnchor.constraint(equalTo: reusableView.leadingAnchor).isActive = true
self.reusableViewContainer.trailingAnchor.constraint(equalTo: reusableView.trailingAnchor).isActive = true
}
}
Of course, you don't have to use a container view. Place it wherever you need it in your view hierarchy, and lay it out as you do for other views.

Determine if `UIView` is loaded from XIB or instantiated from code

I use the following code to force a subclass of UIView to load from a XIB file whose name is the actual class name:
class NibView : UIView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
guard isRawView() else { return self }
for view in self.subviews {
view.removeFromSuperview()
}
let view = instanceFromNib()
return view
}
func isRawView() -> Bool {
// What here?
}
}
The purpose of the isRawView() method is to determine whether this view has been created from code, or it's been loaded from the corresponding XIB file.
The implementation I've used so far is:
func isRawView() -> Bool {
// A subview created by code (as opposed to being deserialized from a nib)
// has 2 subviews, both implementing the `UILayerSupport` protocol
return
self.subviews.count == 2 &&
self.subviews.flatMap(
{ $0.conforms(to: UILayoutSupport.self) ? $0 : nil }).count == 2
}
which uses a trick to determine if the view is created from code, because in such cases it contains exactly 2 subviews, both implementing the UILayoutSupport protocol.
This works nicely when a NibView subclass is instantiated from code. However it doesn't work if the view is created as part of a view controller in a storyboard (and presumably the same happens for view controllers and views loaded from XIB files).
Long story to explain the reason of my question: is there a way for a UIView to know whether it's been loaded from a XIB file, and possibly the name of that file? Or, otherwise, an alternative way of implementing the isRawView() method, which should:
return false if the view has been deserialized from an associated XIB file (whose name is the class name)
return true otherwise
Make use of the provided init functions.
init(frame:​) -> From code
init(coder:​) -> From nib
Example code:
override init(frame: CGRect) {
super.init(frame: frame)
print("From code")
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("From nib")
}
I can print out the class name like this:
print(NSStringFromClass(type(of: self)).components(separatedBy: ".").last ?? "Couldn't get it")
You should be able to use that, maybe with some slight adjustments, to get what you need.

Custom UIView subclass with XIB in Swift

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.

Resources