How can I add a UITableView as a subview to a UIView? - ios

First, this is possible and is this a good idea?
I don't want the UITableView to fill up the whole controller and I want to control the location of this view as I am learning. I have tried searching for tutorials showing this, but I am coming up empty handed as I have tried to implement this functionality.
So, I have a ChildView that subclasses UIView. This view implements or conforms to the necessary UITableView protocols such that I can set the delegate and dataSource to the ChildView. However, I can't seem to make it work properly. Can someone perhaps help me answering why? This is my first time working with UITableViews, so I don't have much experience yet. Working on it! :)
class ChildView: UIView
{
let childTableView = UITableView()
override init(frame: CGRect)
{
super.init(frame: frame)
childTableView.register(ChildTableViewCell.self, forCellReuseIdentifier: "cellId")
childTableView.delegate = self
childTableView.dataSource = self
childTableView.translatesAutoresizingMaskIntoConstraints = false
addSubview(childTableView)
childTableView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
childTableView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0).isActive = true
childTableView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0).isActive = true
childTableView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
setupLayout()
}
private func setupLayout()
{
self.layer.cornerRadius = 13
self.backgroundColor = UIColor.white
self.layer.borderWidth = 2
self.layer.borderColor = UIColor.black.cgColor
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}
extension ChildView: UITableViewDataSource, UITableViewDelegate
{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
return tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
}
}
class ChildTableViewCell: UITableViewCell
{
override init(style: UITableViewCellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
private func setupViews()
{
// Nothing yet.
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Edit 1: Included an image of the view that I am trying to fill with a UITableView.
Edit 2: I have added the code that makes it work in the ChildView class.

From an architectural point of view, you shouldn't do this. A view should only display data that it get's from somewhere outside. It should not act as a data source and try to get it's own data. If somebody reads your code (imaging yourself in one year), no-one would expect a view to behave in the way you implemented it.
Then, you do not display the table view anywhere. You'll have to add it as a subview to self and setup its frame / autolayout constraints to react on resizing etc.
If you want to separate the data source code from the view controller, you should create your own class which implements UITableViewDataSource, instantiate it, set it as the data source to the table view, and there you go. Separating the delegate would work the same, but maybe it is better to keep it in the view controller, because this acts as a co-ordinator between all the views in it.

Here is Sample code Posted , You can check this out Adding two tableViews in Different Views as Subview , used parent view is a ContainerView but you cause same code and just add subview in Your Normal UIView
Reference Code :
/// Class Obj
private lazy var FirstObject: firstVC =
{
// Instantiate View Controller
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "firstVC") as! firstVC
// Add View Controller as Child View Controller
self.addChildViewController(viewController)
return viewController
}()
/// Adding it as Subview:
private func add(asChildViewController viewController: UIViewController)
{
// Configure Child View
viewController.view.frame = CGRect(x: 0, y: 0, width: self.firstContainer.frame.size.width, height: self.firstContainer.frame.size.height)
// Add Child View Controller
addChildViewController(viewController)
viewController.view.translatesAutoresizingMaskIntoConstraints = true
// Add Child View as Subview
firstContainer.addSubview(viewController.view)
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
GitHub Repo - https://github.com/RockinGarg/Container_Views.git

Related

How to Re-use an xib subclass of UIView in UITableViewCell

So I'm a newbie and trying some reusability. I've a class called SingleButtonFooterView which subclasses UIView and UI is done in an .xib.
Now I want to use this in a UITableViewCell. I've tried almost all possible solutions nothing is working for me.
Code for the class:
class SingleButtonFooterView: UIView {
#IBOutlet weak var button: Button50!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
let view = UINib(nibName: "SingleButtonFooterView", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
view.frame = self.bounds
self.addSubview(view)
}
override func awakeFromNib() {
super.awakeFromNib()
button.backgroundColor = .clear
button.layer.cornerRadius = 5
button.layer.masksToBounds = true
button.titleLabel?.font = UIFont.futura(with: .medium, size: 16)
button.setTitleColor(.white, for: .normal)
self.contentMode = .scaleAspectFit
self.layer.masksToBounds = true
}
}
Now for cellForRowAt:
let cellId = "SingleButtonFooterView"
var cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
if cell == nil {
cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: cellId)
let subView = SingleButtonFooterView(frame: cell.frame)
cell.contentView.attachViewWithConstraints(subView)
let _ = subView.viewLoadedFromNibAttached(name: cellId)
}
return cell
and in viewDidLoad() of my VC class.
tableView.register(UINib(nibName: "SingleButtonFooterView", bundle: nil), forCellReuseIdentifier: "SingleButtonFooterView")
In short
-> I want to use a UIView class (UI done using interface builder -> .xib) in a UITableViewCell
If I can suggest you something just do it "by the book".
Create a custom cell and .xib. Then you can do whatever you want with the UIViews (remember that you can create your own class and put it into xib by changing the class here:
Having awakeFromNib and Init in one class it's somehow a code smell because either you use .xib or code to create a view.
Remember that adding subviews from the VC it's always risky because you need to take care of recycle, what means that this subview may stay and be not wanted unless you handle this situation.
Remember about prepareForReuse() to handle cells' recycle.
You have two issues here:
1)
Bundle.main.loadNibNamed(name, owner: self, options: nil) in viewLoadedFromNibAttached (if you use the exact same code from the other question)
and you have the same nib load in commonInit.
You have to decide where to put it, IMO you can get rid of
let subView = SingleButtonFooterView(frame: cell.frame)
cell.contentView.attachViewWithConstraints(subView)
let _ = subView.viewLoadedFromNibAttached(name: cellId)
and put that part in the cell (this way you can easily maintain the livecycle of the SingleButtonFooterView
2) Guess: the File owner of the xib is empty or using wrong class, that's why commonInit and required init?(coder aDecoder: NSCoder) are causing infinite loop
EDIT:
Step 1: subclass UITableViewCell, lets call it SingleButtonCell
Step 2: custom view (SingleButtonFooterView in your case). NOTE! File's owner should be the class itself -> SingleButtonFooterView
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
loadXib()
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadXib()
}
private func loadXib() {
Bundle.main.loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)
addSubview(contentView)
contentView.frame = bounds
}
This is what I use to load views from .xib. contentView is the outlet of the main view in the .xib file
Step 3: Two options here:
Option 3.1: (easier to maintain IMO)
With the SingleButtonCell you create it's own .xib file
3.1.1: Add view in the cell's xib (SingleButtonCell.xib) and change it's class to SingleButtonFooterView
Option 3.2: Do not create cell xib. Instead instantiate the view (SingleButtonFooterView) inside the cell and add it as subview (add constraints if you want). Here you have to be careful where to instantiate the view, because there is a chance to add it multiple times
What I can see is that dequeueReusableCell(withIdentifier:for:) never returns nil. So the code in your nil-check will never be called. You may be thinking of the "old" dequeueReusableCell(withIdentifier:) which can return nil and can be used similar to how you do.
Since it's never nil, you need an additional parameter, say "hasBeenInitialized" to keep track of if you have added your custom UIView yet or not.
// In loading viewDidLoad, register cellId for type for tableView
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
if !hasBeenInitialized {
// Load view from xib
// Add view as subview to cell contentview
// Add surrounding constraints
hasBeenInitialized = true
}
return cell

Referencing superview inside of view code class file

I'm currently trying to learn constraints and styling programmatically in Swift. I'm also trying to maintain clean and modularized code by splitting up code that relates to "styling".
I simply have my LoginViewController:
import UIKit
class LoginViewController: UIViewController {
var loginView: LoginView!
override func viewDidLoad() {
super.viewDidLoad()
loginView = LoginView(frame: CGRect.zero)
self.view.addSubview(loginView)
// AutoLayout
loginView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .bottom)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
Then my LoginView:
import UIKit
class LoginView: UIView {
var shouldSetupConstraints = true
var headerContainerView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
// Header Container View
headerContainerView = UIView(frame: CGRect.zero)
headerContainerView.backgroundColor = UIColor(red:0.42, green:0.56, blue:0.14, alpha:1.0) // #6B8E23
headerContainerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(headerContainerView)
headerContainerView.topAnchor.constraint(equalTo: self.superview!.topAnchor)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func updateConstraints() {
if(shouldSetupConstraints) {
// AutoLayout constraints
shouldSetupConstraints = false
}
super.updateConstraints()
}
}
Where I am getting stuck is with just simply trying to add this headerContainerView to the top of my superview. I want to be able to add it so it pins itself to the top, left and right of the superview and only 1/3 of the superview's height. I continue to try and reference the superview with no success and I cannot find a solution that helps me understand on the internet. Any suggestions on how I can complete this?
Thank you for taking the time for those that respond.
NOTE: I did start out using PureLayout which is really nice. However, I am an individual that likes to understand what is going on behind the scenes or at least how to write the code at its base level. You can see that I am using a PureLayout function in my LoginViewController, but I am looking to change that. I would prefer a solution that doesn't add a third party library.
Here self in the custom UIView class is the parent view of headerContainerView so , You can add this , Also I recommend to learn constraints first without 3rd party libraries to fully understand the concept as you will learn a lot from seeing conflicts and other things , once done , shift to libraries
override init(frame: CGRect) {
super.init(frame: frame)
// Header Container View
headerContainerView = UIView(frame: CGRect.zero)
headerContainerView.backgroundColor = UIColor(red:0.42, green:0.56, blue:0.14, alpha:1.0) // #6B8E23
headerContainerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(headerContainerView)
headerContainerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
headerContainerView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
headerContainerView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
headerContainerView.heightAnchor.constraintEqualToAnchor(self.heightAnchor, multiplier:1.0/3.0, constant: 0.0).active = true
}
// loginView layout
override func viewDidLoad() {
super.viewDidLoad()
loginView = LoginView(frame: CGRect.zero)
self.view.addSubview(loginView)
loginView.translatesAutoresizingMaskIntoConstraints = false
loginView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
loginView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
loginView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
loginView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
headerContainerView.snp.makeConstraints { (make) in
make.top.equalTo(self)
make.leading.and.trailing.equalTo(self)
make.height.equalTo(self.frame.height/3)
}
With SnapKit.
With SnapKit, you can do the following:
override func viewDidLoad() {
super.viewDidLoad()
loginView = LoginView(frame: CGRect.zero)
self.view.addSubview(loginView)
// AutoLayout
loginView.snp.makeConstraints { (make) in
make.left.equalTo(view.snp.left)
make.right.equalTo(view.snp.right)
make.top.equalTo(view.snp.top)
make.height.equalTo(view.snp.height).multipliedBy(1/3)
}
}

Selecting collection view cell programmatically cannot do some of the cell's isSelected routines

I have this code to change the color and font style of a cell when it is selected. It worked except that when I select it manually from the parent view's init(frame:) function. It supposed to change the color to blue and font style to bold and it worked perfectly when I toggled it from the test device. However, when I tried to set the default selected item programmatically, it only changed the style to bold but the color is still the same as unselected, ie. gray.
This is the cell's class:
class ItemCollectionCell: UICollectionViewCell {
#IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func setupCell(title: String)
{
titleLabel.text = title
titleLabel.textColor = UIColor.gray
}
override var isSelected: Bool{
didSet {
titleLabel.textColor = isSelected ? UIColor.blue : UIColor.gray
titleLabel.font = isSelected ? UIFont.boldSystemFont(ofSize: titleLabel.font.pointSize) : UIFont.systemFont(ofSize: titleLabel.font.pointSize)
}
}
}
The parent view that has the collection view in it:
class ItemsView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
let reuseableCellId = "itemCell"
let nibItemCell = UINib(nibName: "ItemCollectionCell", bundle: nil)
//Collection view for items
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.dataSource = self
cv.delegate = self
//Create subview (collection) for the menu buttons
//Register cell's XIB and class
cv.register(nibItemCell, forCellWithReuseIdentifier: reuseableCellId)
//Set subview BG color
cv.backgroundColor = UIColor.white
cv.contentInsetAdjustmentBehavior = .never
return cv
}()
override init(frame: CGRect) {
super.init(frame: frame)
//Add subview
self.addSubview(collectionView)
//Set subview constraints
self.addConstraintsWithFormat("H:|[v0]|", views: collectionView)
self.addConstraintsWithFormat("V:|[v0]|", views: collectionView)
let selectedPath = IndexPath(item: 0, section: 0)
collectionView.selectItem(at: selectedPath, animated: false, scrollPosition: .centeredVertically)
}
//... all the other funcs
}
What went wrong here? How do I fix this? Thanks.
It doesn't look like the collectionview is reloaded prior to selecting the cell. Plus, have you tried manually updating isSelected?
After meddling with the code myself, I found out that the setupCell(title:) function is the culprit. FYI, the setupCell is called in the cellForItemAt() delegate function. And when I moved the titleLabel.textColor = UIColor.gray line to awakeFromNib() it works fine now. Thanks anyway guys.

Use the same view inside a collectionViewCell and tableViewCell

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.

Custom collection view cell disappears on tap

I'm working on an iPad app that should display 4 CollectionViews next to each other. The height of the collection views should be 3/4 of the screen, so the layout will look something like this:
___________________
| top content |
|-------------------|
| CV | CV | CV | CV |
|____|____|____|____|
I tried to narrow everything down and created a new project with just one of these collection views, but I keep running into the same problem: when I tap on one of the cells, all of them disappear. To reproduce:
create a new project with template "Single View Application" using Swift as language
setup the storyboard:
drag a new Collection View Controller in the storyboard
set the storyboard ID to "CollectionViewController"
for the cell: set identifier to MyCollectionViewCell, drag a label in the cell, set constraints
create files with the following source code:
CollectionViewCell.swift: (create the outlet by ctrl-dragging the label to the source code)
import UIKit
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
}
CollectionViewController.swift: (pay attention to the comment in viewDidLoad implementation)
import UIKit
private let reuseIdentifier = "MyCollectionViewCell"
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
// this has to be removed to work with a custom cell class
// self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CollectionViewCell
cell.label.text = "\(indexPath.section)-\(indexPath.row)"
return cell
}
}
change the implementation for ViewController.swift:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let frame = view.frame
let vc = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "CollectionViewController")
vc.view.frame = CGRect(x: 0.0, y: frame.size.height * 0.25, width: frame.size.width, height: frame.size.height * 0.75)
self.view.addSubview(vc.view)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here's a capture of the iOS simulator:
https://youtu.be/VVBsTnYLGM4
I hope I didn't forget anything to reproduce the problem. Just in case, I uploaded the project to my dropbox.
https://dl.dropboxusercontent.com/u/607872/CollectionViewBugZip.zip
Can someone tell me what I'm doing wrong?
Turns out the mistake was in adding the collection view as a subview, which isn't considered good practice. Appearently, when only the subview is added, it's "disconnected" from its view controller.
This did the trick:
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let frame = view.frame
let vc = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "CollectionViewController")
self.addChildViewController(vc) // <----------- !!!!
vc.didMove(toParentViewController: self) // <-- !!!!
vc.view.frame = CGRect(x: 0.0, y: frame.size.height * 0.25, width: frame.size.width, height: frame.size.height * 0.75)
self.view.addSubview(vc.view)
}
Check that your ViewController not deiniting. Write deinit { print("Deinited") } and place a breakpoint there. In my case CollectionView disappeared because it was been deiniting with viewController that holt it.
Swift 5 / iOS 16
Adding the following two lines, right below where I programmatically instantiate the UIViewController that hosts the UICollectionView worked for me).
I don't know why it works, but I was completely out of ideas and found this on reddit, and it solved it after a couple of hours of me sweating and trying everything I could think of:
self.addChild(vcThatHostsCollectionView)
vcThatHostsCollectionView(toParent: self)
This is the code in my 'consumer' viewcontroller that instantiates the collection view hosting viewcontroller, that includes the two lines shown above.
func configurePagerTabs() {
let rect = CGRectMake(0, 0, UIScreen.main.bounds.width, 35.0)
let pagerTabsViewController = PagerTabsViewController(frame: rect, tabTitles: tabTitles)
self.addChild(pagerTabsViewController)
pagerTabsViewController.didMove(toParent: self)
pagerTabsViewController.delegate = self
pagerTabsView = pagerTabsViewController.view
view.addSubview(pagerTabsView)
}
This is part of the viewcontroller that creates the collection view, to show how it initializes things.
class PagerTabsViewController: UIViewController {
enum Section { case main }
var dataSource : UICollectionViewDiffableDataSource<Section, String>! = nil
var collectionView : UICollectionView! = nil
var tabTitles : [String]! = nil
var delegate : PagerTabsDelegate? = nil
var reuseId = "TabCellReuse"
var frame = CGRect.zero
required init(coder: NSCoder) { fatalError("Not implemented to use with storyboard") }
#objc init(frame: CGRect, tabTitles: [String]) {
super.init(nibName: nil, bundle: nil)
self.tabTitles = tabTitles
self.frame = frame
}
override func loadView() {
collectionView = UICollectionView(frame: frame, collectionViewLayout: createLayout())
collectionView.isPagingEnabled = true
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .blue
registerCells()
view = collectionView
}
.
.
.
}

Resources