Use the same view inside a collectionViewCell and tableViewCell - ios

I created a nib file with a custom collectionViewCell and attached to a viewController
class CustomCollectionView: UICollectionViewCell{}
Now I have to use the exact cell inside a tableView. I created a new nib file and viewController
class CustomTableView: UITableViewCell{}
and I copied the hole code of CustomCollectionView on it. every thing is working fine but I believe that it dose not make sense to copy the hole exact code of CustomCollectionView into CustomTableView and to use the exact same nib file but with a tableViewCell instead of collectionViewCell on it. Is there any way to optimize what I did?

As you said in a comment in suhit's answer, you can do this by using a common view in both the CollectionViewCell and TableViewCell subclasses. You don't need a ViewController since it adds extra overhead. A simple UIView is enough. Some code to show what I mean:
class CustomTableViewCell: UITableViewCell {
var customView: CustomView!
func awakeFromNib() {
super.awakeFromNib()
customView = CustomView()
customView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customView)
customView.fillSuperview()
}
}
class CustomCollectionViewCell: UICollectionViewCell {
var customView: CustomView!
func awakeFromNib() {
super.awakeFromNib()
customView = CustomView()
customView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customView)
customView.fillSuperview()
}
}
extension UIView {
func fillSuperview() {
guard let superview = superview else {
return print("no superview")
}
topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
leftAnchor.constraint(equalTo: superview.leftAnchor).isActive = true
rightAnchor.constraint(equalTo: contentVisuperviewew.rightAnchor).isActive = true
}
}
A sample implementation for the CustomView class:
class CustomView: UIView {
func initialize() {
//...
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
}
If you wish to create your custom view in a xib that's also fine, but it's a little trickier. This is beyond the scope of the question but I'm just going to leave a link here in case you need it.

If you want to use same view then its better to use similar type view i.e. use collectionView at both places so that you can use the CustomCollectionViewCell in both ViewControllers. UICollectionView is highly customisable so you can do whatever you want to do with UITableView in UICollectionView as well.

Related

Storyboard xib live view renders all subviews in top-left corner

I am using XCode 12.1 and I am trying to create some reusable XIBs and want to render them in the storyboard. My problem is that all those views will be rendered in the top-left corner, just like if the frame of my loaded view is CGRect.zero. The view controller in the storyboard looks like this:
I have made a subclass of UIView that dynamically loads a view from .xib:
import UIKit
#IBDesignable
class NibView: UIView {
required init?(coder: NSCoder) {
super.init(coder: coder)
self.createNibView()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.createNibView()
}
private func createNibView() {
guard let view = loadFromNib() else { return }
self.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(view)
self.createConstraints(for: view)
self.setNeedsLayout()
}
private func loadFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as? UIView
view?.translatesAutoresizingMaskIntoConstraints = false
return view
}
private func createConstraints(for view: UIView) {
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: view.topAnchor),
self.bottomAnchor.constraint(equalTo: view.bottomAnchor),
self.leadingAnchor.constraint(equalTo: view.leadingAnchor),
self.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
and a subclass of this view:
import UIKit
#IBDesignable
class RDTextField: NibView {
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var errorLabel: UILabel!
#IBOutlet weak var errorIcon: UIImageView!
}
with the associated xib:
In storyboard, I have added a view for which I've set the class of this custom view, which is the selected view from the image with the storyboard.
I have also tried playing with awakeAfter(using coder:) with no better results, I tried to use the autoresizing mask, which is slightly better, in the sense that it sets the origin correctly, but it does not have the correct size. Anyway, I would like to use translatesAutoresizingMaskIntoConstraints disabled and use my own constraints.
I have also tried to debug these views, but XCode won't attach to any process and I cannot find the logs inside the Console.app.
Note: I have an IBDesignable extension for UIView which helps me setting border color, width and radius. I have tried to disable it, but without any results.
Note 2: At runtime everything works properly, but live view in storyboard is pretty much needed as well.
Did anyone encounter this or have any idea how to approach this issue?
The root view of a Storyboard object always uses an Autoresizing Mask. Your code is disabling that.
Remove one line from your createNibView() func in your NibView class
private func createNibView() {
guard let view = loadFromNib() else { return }
// don't do this!
//self.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(view)
self.createConstraints(for: view)
self.setNeedsLayout()
}
That should do it.

IBAction for UIButton with it's own .xib file

I want to create a reusable button all over my app and was planning to design it with it's own .xib file. The issue is that I can't connect an IBAction to the custom button in the controllers where it's used.
I created a new .xib file called SampleButton.xib and added a button. This is what the hierarchy and the view looks like:
I then created a new swift file called SampleButton.swift with a class called SampleButton that's a subclass of UIButton and assigned it as the File's Owner in my SampleButton.xib file.
The contents of SampleButton.swift are as follows:
import Foundation
import UIKit
#IBDesignable
class SampleButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
guard let view = loadViewFromNib() as? UIButton else {
return
}
view.frame = bounds
view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth,
UIView.AutoresizingMask.flexibleHeight]
addSubview(view)
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.white.cgColor
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIButton
}
#IBAction func pressed(_ sender: Any) {
print("Called in here")
}
}
I can then create a new button in my storyboard and set it to custom and the class to SampleButton. However now if I ctrl + drag from my button to my corresponding View Controller to create an IBAction for the button, it's not called. The one in the SampleButton.swift file is. Even if I delete the IBAction in the SampleButton file it's still not called.
Any help here? I want to be able to design the buttons separately and then have IBactions for them in the controllers where they're used.
I encountered this same issue with some of my custom xib views and my initial thought was that I could set up my xib to be IBDesignable and then connect outlets from the storyboard rendering of my button in the view controller.
That didn't work.
So I setup a bit of a workaround using delegate callbacks from my custom views. I created IBOutlets for the view to the view controllers using them, then in viewDidLoad I'd set the delegate and handle the button tap in the view controller
import UIKit
// defines a callback protocol for the SampleButtonView
protocol SampleButtonViewDelegate: class {
func sampleButtonTapped(_ button: SampleButton)
}
#IBDesignable
class SampleButton: UIView, NibLoadable {
// create IBOutlet to button if you want to register a target/action directly
#IBOutlet var button: UIButton!
// set delegate if you want to handle button taps via delegate
weak var delegate: SampleButtonViewDelegate?
// initializers to make it so this class renders in view controllers
// when using IBDesignable
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
loadFromNib(owner: self)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadFromNib(owner: self)
}
#IBAction func buttonTapped(_ sender: Any) {
delegate?.sampleButtonTapped(_ button: self)
}
}
// here's a sample ViewController using this view and the delegate callback method
class ViewController: UIViewController {
#IBOutlet var sampleButtonView: SampleButton!
override func viewDidLoad() {
super.viewDidLoad()
sampleButtonView.delegate = self
}
}
extension ViewController: SampleButtonViewDelegate {
func sampleButtonTapped(_ button: SampleButton) {
// TODO: run logic for button tap here
}
}
For completeness I'll also add this NibLoadable protocol definition here.
// I used this for the #IBDesignable functionality to work and actually render
// my xib layouts in the storyboard view controller layouts using this class
import UIKit
/// Defines an interface for UIViews defined in .xib files.
public protocol NibLoadable {
// the name of the associated nib file
static var nibName: String { get }
// loads the view from the nib
func loadFromNib(owner: Any?)
}
public extension NibLoadable where Self: UIView {
/// Specifies the name of the associated .xib file.
/// Defaults to the name of the class implementing this protocol.
/// Provide an override in your custom class if your .xib file has a different name than it's associated class.
static var nibName: String {
return String(describing: Self.self)
}
/// Provides an instance of the UINib for the conforming class.
/// Uses the bundle for the conforming class and generates the UINib using the name of the .xib file specified in the nibName property.
static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
/// Tries to instantiate the UIView class from the .xib file associated with the UIView subclass conforming to this protocol using the owner specified in the function call.
/// The xib views frame is set to the size of the parent classes view and constraints are set to make the xib view the same size as the parent view. The loaded xib view is then added as a subview.
/// This should be called from the UIView's initializers "init(frame: CGRect)" for instantiation in code, and "init?(coder aDecoder: NSCoder)" for use in storyboards.
///
/// - Parameter owner: The file owner. Is usually an instance of the class associated with the .xib.
func loadFromNib(owner: Any? = nil) {
guard let view = Self.nib.instantiate(withOwner: owner, options: nil).first as? UIView else {
fatalError("Error loading \(Self.nibName) from nib")
}
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
}
You could also simply register the functions you defined in your view controller as the target/action functions for the button in the custom view.
override func viewDidLoad() {
super.viewDidLoad()
mySampleButtonView.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) {
// handle button tap action in view controller here...
}
create iboutlet of button in nib class.
add you nib view in your viewcontroller where its needed.
add target for the button outlet.
try following code:
override func viewDidLoad() {
super.viewDidLoad()
let myButton = Bundle.main.loadNibNamed("myButtonxibName", owner: self, options: nil)?[0] as? myButtonxibClassName
myButton.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
self.view.addsubview(myButton)
}
#objc func buttonTapped() {}
You don't need a Xib for what you're trying to do. Remove the loadViewFromNib() and the pressed(_ sender: Any) functions from your class above. Change your setup() method to customize your button. I see that you want to add a border to it. Do something like this:
func setup() {
self.layer.borderWidth = 2
self.layer.borderColor = UIColor.white.cgColor
// * Any other UI customization you want to do can be done here * //
}
In your storyboard, drag and drop a regular UIButton wherever you want to use it, set the class in the attributes inspector to SampleButton, connect your IBOutlet and IBActions as necessary, and it should be good to go.
I don't think it's possible to do this. Simpler way is to just set the target and action in view controllers. Something like:
class VC: UIViewController {
func viewDidLoad() {
sampleButton.addTarget(self, action: #selector(didClickOnSampleButton))
}
}

Deinit never called on custom UIView

Hello I have a UIViewController which has 4 custom UIView. Deinit function inside UIViewController is called when I change the rootViewController but doesn't called inside custom UIView. Therefore, It persists in memory after I change root controller. If I call myCustomView.removeFromSuperView() inside ViewControllers Deinit function, It works. However, I don't wanna write it everytime in a viewcontrollers so I want to write it inside custom UIView class but Deinit inside Custom UIView never get called.
How I declare custom view:
lazy var myCustomView = WelcomeScreenButtonView(text: getLabelText(key: SUPPORTEDSERVICES), imageName: "img")
let firstStackView = UIStackView()
override func viewDidLoad(){
//I put it inside a UIStackView
firstStackView.addArrangedSubview(myCustomView)
view.addSubView(firstStackView)
}
deinit{
//If I do it like this, It works but I don't wanna call it in here
myCustomView.removeFromSuperView()
}
My Custom UIView Class:
class WelcomeScreenButtonView: UIView {
lazy var label = UILabel()
private lazy var logoImage = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
//This one never get called
print("WelcomeScreen Button Deinited")
removeFromSuperview()
}
}
EDIT: If I change my custom view to UIView() (all other codes are same). It is deallocating. Therefore, I guess I have something to do with custom UIView class.

xcode: label not showing after adding constraint

I have a collectionViewCell, I added to it one label, I set constraint spacing to nearest neighbor to (0,0,0,0), so the must take all cell and adjust to its height and width, but the label is not showing at all.
I realized that what ever I put in a collectionCell, if I added to it a constraint, It well not be showing, so I ended up deleting all constraints and manually set label's height and width
CollectionViewCell:
import UIKit
class CollectionViewCell: UICollectionViewCell {
var text:String?
var delegate: TableViewCell?
#IBOutlet weak var label: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
loadFromNib() // load xib
let tap = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tapFunc(_ sender: Any) {
// head will not be clickable cuz parent here gonna be nil
if let p = delegate {
if p.isUserInteractionEnabledWith(cell: self){
p.didTapeLabel(for: self, value: label.text!)
}
} else {
print("parent at collectionview cell is nil")
}
}
func fillOutData(_ text:String) {
label.text = text
}
}
enter image description here
I am building a multicolumn tableView :
tableView --> tableViewCell --> CollectionView --> CollectionViewCell --> label
Set data source and delegate for UICollectionView inside UITableViewCell.
override func awakeFromNib() {
//collection View Delegates
self.collectionView.delegate = self
self.collectionView.dataSource = self
super.awakeFromNib()
}

Is it possible to use IBDesignable with UICollectionViewCells and live render them in storyboard?

I know how to use #IBDesignable with custom views.
but is it possible to use IBDesignable for cells and render them in storyboard?
for example: i have a collectionViewController in storyboard, and added a uiCollectionCell and specified class as my customCellClass.
p.s: i know for using Xibs in collecionViews and tableViews we have to call method registerNib:forReuseIdentifer in code (and i am doing it). just wondered, is it possible to see it's rendered view in storyboard or not.
p.s2: i found this and it works perfectly with UIViews, but don't know how to make it work with CollectionCells and TableCells. :(
Yes. Here is what I found with Xcode 10.1 and iOS 12. Adding #IBDesignable to the custom subclass of UICollectionViewCell did work intermittently, but this works more reliably:
Add #IBDesignable to a custom subclass of UIView
Override layoutSubviews(), and define the appearance there
Optionally, if you want to define dummy data for IB only, override prepareForInterfaceBuilder()
Add that custom view to your prototype UICollectionViewCell in Interface Builder
You should see Interface Builder "build" the views and draw your change in your customer view (I find this unreliable, too. If nothing happens and Menu / Editor / Automatically Refresh Views is checked, make some other change in Interface Builder)
Example Class
#IBDesignable
class Avatar: UIView {
// Despite not being used for views designed in Interface Builder, must still be defined for custom UIView subclasses with #IBDesignable, or IB will report errors
override init(frame: CGRect) {
super.init(frame: frame)
}
// Used when a view is designed inside a view controller scene in Interface Builder and assigned to this custom UIView subclass
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.bounds.width / 2
self.backgroundColor = UIColor.gray
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.backgroundColor = UIColor.blue
}
}
Yep. Here's how I did it.
First make sure the File Owner of NIB file is set to your custom cell class. Check this
Override the prepareForInterfaceBuilder method and add the contentView from NIB file in the contentView of prototype cell. This is what it looks like.
// ArticleTableViewCell.swift
import UIKit
#IBDesignable
class ArticleTableViewCell: UITableViewCell {
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var authorImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var dateCreatedLabel: UILabel!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
addNIBContentView(toView: contentView)
}
private func addNIBContentView() {
let view = loadContentViewFromNib()
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(view)
}
private func loadContentViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
// Make sure your NIB file is named the same as this class, or else
// Put the name of NIB file manually (without the file extension)
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
Now set the custom class of the prototype cell in your table view to the one above and refresh the views.
Editor > Refresh All Views
If it still does not show up. Just clear the build folder and refresh all views again.
Product > Clean Build Folder
I made a handy extension for myself to reuse the last 2 functions in all UITableViews/UICollectionViews
// ViewExtensions.swift
import UIKit
extension UIView {
func addNIBContentView(toView contentView: UIView? = nil) {
let view = loadContentViewFromNib()
// Use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if let contentView = contentView {
contentView.addSubview(view)
} else {
addSubview(view)
}
}
private func loadContentViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
// Make sure your NIB file is named the same as it's class
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
// ArticleTableViewCell.swift
import UIKit
#IBDesignable
class ArticleTableViewCell: UITableViewCell {
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var authorImage: UIImageView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var dateCreatedLabel: UILabel!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
addNIBContentView(toView: contentView)
}
}
After lots of testing and working with the library I came up with this:
you should not add TableViewCell or CollectionViewCells inside .nib files, instead you have to add simple View. I'm not sure if it's gonna show up inside storyboard or not (haven't checked it yet) but it makes errors go away. Now you can even use autoLayout for self sizing cells.

Resources