UICollectionView with full page cells VS UIScrollView with child view controllers - ios

I'm trying to create a ViewController which would have swipe-able (android like tabs) pages. These pages themselves will have scrollviews(vertical) inside them and multiple views which would be added dynamically depending on type of response (different network calls for each page). I can't use a PageViewController as I want the pages to take up only half the screen.
Issues with CollectionView -
If the cells would get reused (or removed from memory), how would I maintain the state of the cell's UI and store that data (especially difficult for views as each page might have different type of view in them)
Issues with ScrollView -
I'm worried if there would be memory issues if all page view controllers would be in memory with each view in it
PS - data in each page would be 4-10 stackviews each containing 2-10 images/labels OR just one collectionview
PSS - Total number of tabs wouldn't exceed 10, minimum would be 1

I'd implemented it with collectionView cause it should be really more resource effective. But then we need to cache states of view controllers. Here is the example
Let's say you have controller A which contains collectionView with cell with your child controllers. Then in cell for row
....
var childrenVC: [Int: UIViewController] = [:]
....
// cell for row
let cell: ChildControllerCell = collectionView.dequeueReusableCell(for: indexPath)
if let childController = childrenVC[indexPath.row] {
cell.contentView.addSubview(childController.view)
childController.view.frame = cell.contentView.frame
} else {
let childViewController = ChildViewController()
addChildViewController(childViewController)
childViewController.didMove(toParentViewController: self)
cell.contentView.addSubview(childController.view)
childController.view.frame = cell.contentView.frame
childrenVC[indexPath.row] = childViewController
cell.childVC = childViewController
}
return cell
....
class ChildControllerCell: UICollectionViewCell {
var childVC: UIViewController?
override func prepareForReuse() {
super.prepareForReuse()
if !contentView.subviews.isEmpty {
childVC?.willMove(toParentViewController: nil)
childVC?.view.removeFromSuperview()
childVC?.removeFromParentViewController()
}
}
}

Related

Collection View inside UITableView reuse issue

I have a TableView with a CollectionView inside.
So each Table cell is the delegate of the CollectionView.
The problem is that the collection View reloads on the main thread and the UITableView Cells are being reused.
That causes the collectionView cells to briefly present the labels of previous cells and then fade the changes.
How can I hide those labels when the tableView is initially passed through the return cell ?
The problem happens as collection.reload() is applied after the return cell of the UITableView in cell for row.
Here is a sample of the code.
UITableViewCell Code
let collectionDatasource = CollectionDataSource()
func configureCell(_ object: CustomClass) {
//Configure Object for tableView
self.presentedObject = object
self.nameLabel.text = object.name
//Set object and items on collection datasource
DispatchQueue.main.async {
let collectionItems = object.collectionItems ?? []
self.collectionDatasource.object = object
self.collectionDatasource.items = collectionItems
}
}
class CollectionDataSource: NSObject {
private var collectionView: UICollectionView?
var items: [CustomItem] = [] {
didSet {
DispatchQueue.main.async {
let changes = [CustomItem].changes(from: oldValue, to: self.items, identifiedBy: ==, comparedBy: <)
self.collectionView?.apply(changes: changes)
}
}
}
}
UICollectionView itself does its work asynchronously in contrast to UITableView. So, even if you will call reload collectionView directly in configureCell it will update the collection's cells after the table is set up (still can result in the mentioned issue). But you have added the "reload" call into DispatchQueue.main.async which makes the things worse.
You should skip using DispatchQueue.main.async if possible.
You should replace collectionView with a new one in configureCell. I suggest to add view into the cell of the same size and position as collection view and add collection view in the code. This will guarantee that previous collection view will never appear. 👍

How to properly manage memory while using child view controllers in UICollectionView cells?

I have UIViewController(HomeController) with horizontally scrollable UICollectionView in it. This collection view contains 2 cells (the number is not dynamic, there will be always only 2 cells). The cell covers all the screen. In each cell I have a child view controller, which has a UICollectionView itself and scrolls vertically. The cells of this inner collectonView contain big images (cover whole size of a cell).
I set up my child view controllers in HomeController's viewDidLoad like this:
addChild(viewController)
viewController.didMove(toParent: self)
To add child controller's view as a subview to my cells I have hostedView property in the cell's class:
class ContainerCell: UICollectionViewCell {
var hostedView: UIView? {
didSet {
guard let hostedView = hostedView else {
return
}
contentView.addSubview(hostedView)
hostedView.translatesAutoresizingMaskIntoConstraints = false
hostedView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostedView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
hostedView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
}
}
}
This hostedView of the cell I set up in cellForItemAt method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = containerView.dequeueReusableCell(withReuseIdentifier: ContainerCell.reuseIdentifier, for: indexPath) as! ContainerCell
cell.hostedView = childControllers[indexPath.item].view
return cell
}
childControllers is an array where I store my two child view controllers.
The problem there is when app is launched for the first time and I scroll from the first cell to the second there is a delay in UI's reaction to the scroll. It scrolls not immediately but about 0.5 sec later after I do a scroll gesture. After that back and forth scrolls work fine. As far as I can tell this happens because at the launch the 2nd cell is not visible and cellForItemAt method for it hasn't been run yet. It runs when I scroll to this cell. This is completely fine behavior driven by iOS' reuse mechanism. But seems like because there is child controller in the cell with big images in it there is too much memory needed to be allocated so it causes this delay. At the launch Xcode shows about 100mb in memory, after a scroll to the second cell it grows to almost 200mb.
So even that I initialize all child controllers at viewDidLoad, the memory for them allocates only at cellForItemAt stage.
I would like to allocate that memory at the initial stage, so when I scroll they would be in the memory already. It kind of goes against cell reuse mechanism, but I know that there will always be only two cell (and two child controllers), so I don't really need that dynamic style behavior from my collectionView.
I tried to use different reuseIdentifiers for cells, and assign child controller's view to cell's hostedView in viewDidLoad, not in cellForItemAt, but it hasn't worked.
Is there any way to do that? And if there is not, how to approach this another way? This delay in UI reaction is really ruining user experience of the app.

Use UICollectionViews to create dynamic and multiple features

I'm interested in creating a view that contains multiple features that users can scroll down and see i.e. pictures, description, comments, carousels etc. I am aware that the UICollectionView is able to provide this type of layout. I initially thought UITableViews would be the best approach.
I have looked at several tutorials and GitHub repos but majority of them just use a UICollectionView in a standard grid layout. I've also looked at IGListKit used by Instagram and some the tutorials linked to it.
I'm aiming to get something like the KitchenStories app:
I was wondering if someone could advice me in terms of the direction and approach best for this.
Don't try to do too much with any single view, even a UICollectionView.
The screen you've shown has a UITabBarController manage its top-level arrangement. The currently selected tab (“Home”) has a UINavigationController managing its content.
On the top of the navigation stack is, probably, a collection view or a table view. Either could be used here because the elements are visually laid out as screen-width rows in a stack. A table view is simpler because then you don't have to worry about setting up the layout.
The table view has several visible rows, each different:
The title/image row (“Easy seafood paella”)
The ratings row
The export row (hearts / save / share)
The comments row
The creator row (I assume, since it looks like it's probably a headshot and a name)
And there are probably even more unique rows out of view.
In your storyboard, you can design each of these rows as a prototype row in the table view controller's scene. Or you can design the table view with static content rows, which is easier if you won't need to change the order of the rows or duplicate any rows at run time.
“But Rob,” you say, “I can't fit all those rows into the table view in my storyboard!” Make the storyboard scene taller. UIKit will resize it at run time to fit the device screen.
In each row, drag in and arrange whatever subviews you need for that row's data. For example, the title/image row needs a UIImageView and a UILabel. The ratings row needs a label, probably a custom view to display and edit the stars, and perhaps a stack view for layout.
For each row, you'll need a separate subclass of UITableViewCell with outlets to that row's views. To pass the data to each cell for display, make each cell conform to a protocol:
protocol RecipeUsing {
var recipe: Recipe? { get set }
}
Then, in your table view controller, you set it like this if you're using static content:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if let user = cell as? RecipeUsing {
user.recipe = recipe
}
return cell
}
You'll need a RecipeTitleImageCell with outlets to the UIImageView and UILabel. Something like this:
class RecipeTitleImageCell: UITableViewCell, RecipeUsing {
#IBOutlet var label: UILabel!
// UITableViewCell has an imageView property that's not an outlet 😭
#IBOutlet var myImageView: UIImageView!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
label.text = recipe.title
myImageView.image = recipe.image
}
}
}
And for the ratings row, you'll want something like this:
class RecipeRatingsCell: UITableViewCell, RecipeUsing {
#IBOutlet var ratingControl: RatingControl!
#IBOutlet var label: UILabel!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
ratingControl.rating = recipe.ratings.reduce(0, +) / recipe.Double(ratings.count)
if ratings.count < 5 { label.text = "Too few ratings" }
else { label.text = "\(ratings.count) ratings" }
}
}
}

UITableView & UICollectionView showing same data in 2 different situations

I have a scenario in which I require to display some data in UITableView when user clicks on a particular UIButton and display the same data in UICollectionView when clicked on another UIbutton . But in the same view controller .
First when table view is selected we are going for data in table view -
it would show data in table view format
Second when collection view button is clicked we are going for displaying data in collection view.
I think we can achieve this by the following ways -
1)Creating 2 views in scene dock one having table view and the other having collection view and using them in our code.
2)Creating 2 .xib files of UIView class having TableView & CollectionView and loading them on selection of particular button.
So how could I achieve this functionality? Please suggest some ways which could be quite reliable in such cases and if possible share any source codes or links or give detailed explanation which could help iOS novices like me.
You could use UIPageViewController. Just remember to disable scrolling of pageviewcontroller.
It seems like to make custom view container.
Google with keywords iOS custom container, there are many resources.
Custom container view controller transitions
Container View controller quick start
Apple document, about custom container
You can also add both the UItableView and UICollectionView in the same view and make one of them hidden in each state. So by default the first button is selected where the collectionView will be hidden. When user taps on the second button you can hide the table view and unhide the collection view. You can also add animation for transitioning two state to make it look good.
If we simply think about this problem then we can use only one collectionView with two different kind of Layouts and cellViews of different sizes. But I am not sure, how much will be the complexity with Aujtolayout (if you using).
First create two class file with UITableView and UICollectionView baseClass for more detail see below screenshot.
And add datasource and delegate method for both class in implementation file.
Now add following code in .h(interface) file where you want to show table/collection view.
#import "collectionData.h"
#import "tableData.h"
#interface ViewController : UIViewController {
collectionData *collectionObj;
tableData *tableObj;
IBOutlet UIView *viewData;
}
#end
And in .m(implementation) file:
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *arrCollection = [[NSBundle mainBundle] loadNibNamed:#"collectionData" owner:self options:nil];
collectionObj = (collectionData *)[arrCollection objectAtIndex:0];
NSArray *arrTable = [[NSBundle mainBundle] loadNibNamed:#"tableData" owner:self options:nil];
tableObj = (tableData *)[arrTable objectAtIndex:0];
[self displayView:tableObj];
}
- (IBAction)btnTableClick:(id)sender {
[self displayView:tableObj];
}
- (IBAction)btnCollectionClick:(id)sender {
[self displayView:collectionObj];
}
- (void)displayView: (UIView *)viewObj {
for(UIView *view in viewData.subviews) {
[view removeFromSuperview];
}
[viewData addSubview:viewObj];
}
Note that its not everything yet that you want it's only basic idea.
Your initial view controller can house the buttons and a "Container View Controller" that will segue between a UITableViewController and a UICollectionViewController based on the button that was pressed. When a button is tapped, the initial view controller will pass along the product data and make itself the child view controller's delegate.
When a cell is selected in either of your child view controllers, they can call methods on the delegate (your initial view controller) and it can act accordingly.
To pass data into whatever child view controller you want to display first (e.g. the UITableViewController), perform a segue when the initial view controller loads.
I would personally suggest any one facing such type of situation to go for Scene Dock which many don't care to use. It requires quite less coding and is very easy. I think it can be considered as an alternative for .xib .
I followed some details about scene dock in the link - http://www.raywenderlich.com/115697/ios-9-storyboards-tutorial-whats-new-in-storyboards
Add a table view and a collection view in the scene dock of your view controller and go for the coding. Here is an example of hoe it can be done.
Main story board -
Here goes the coding with all explannations -
import UIKit
class CategoriesViewController: UIViewController,UITableViewDataSource,UITableViewDelegate,UICollectionViewDataSource,UICollectionViewDelegate
{
var mainarray : NSMutableArray!
var cararray : NSMutableArray!
var bikearray : NSMutableArray!
#IBOutlet var TableVieww: UITableView!//table view outlet in scene dock
#IBOutlet var collctnvieww: UICollectionView!//scene dock collection view outlet
#IBOutlet weak var rqdview: UIView!// view on which you would be adding the collection/table view
override func viewDidLoad()
{
super.viewDidLoad()
cararray = ["audii","duster"]
bikearray = ["honda","bajaj"]
TableVieww.dataSource = self
TableVieww.delegate = self
collctnvieww.delegate = self
collctnvieww.dataSource = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func Car(sender: AnyObject)//outlet of car button
{
mainarray = cararray as NSMutableArray
self.TableVieww.reloadData()
self.collctnvieww.reloadData()
}
#IBAction func bike(sender: AnyObject)//outlet of bike button
{
mainarray = bikearray as NSMutableArray
self.TableVieww.reloadData()
self.collctnvieww.reloadData()
}
#IBAction func showcllctnview(sender: AnyObject)//when you want to show collection view for cars/bikes
{
print("collection view called")
collctnvieww.frame = CGRectMake(0, 0, rqdview.frame.width, rqdview.frame.height)//you change the frame size of your views as per your requirement.
self.rqdview.addSubview(collctnvieww)
}
#IBAction func showtableview(sender: AnyObject)//when you want to show table view for cars/bikes
{
print("table view called")
TableVieww.frame = CGRectMake(0, 0, rqdview.frame.width, rqdview.frame.height)
self.rqdview.addSubview(TableVieww)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return mainarray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = TableVieww.dequeueReusableCellWithIdentifier("cellreuse", forIndexPath: indexPath)
cell.textLabel!.text = mainarray[indexPath.row] as? String
return cell
}
//coding for collection view
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return mainarray.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collctnvieww.dequeueReusableCellWithReuseIdentifier("collectioncell", forIndexPath: indexPath) as! CategoriesCollectionViewCell
cell.nmlbl.text = mainarray[indexPath.row] as? String
return cell
}
}
But still I think the code can be shortened if I can use only one data source for table view and collection view. If any one can suggest some tips, it might be helpful.

Scrollview with embedded tableview

I'm building an iOS app in swift with Xcode 6.
I'm trying to embed a view controller with a table view in a scrollview. When the user drags in the table view, it is suppose to move the table, not the the scrollview that it is embedded in.
I've made this illustration, to clearify my view and view controller hierachy:
The red area is the content size area of the scrollview.
The green and blue areas are different view controllers, embedded in the scrollview.
The yellow area is a Text field in the blue view controller.
The orange area is a table view in the blue view controller.
I have enabled paging in the scrollview, so that it snaps to either the green or blue view controller. How can I pop the Table view to the top of the view hierachy, so that the only way to scroll the scrollview, will be to drag in the text field.
import UIKit
class RootViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var greenViewController: GreenViewController!
var blueViewController: BlueViewController!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: CGRectMake(0, 0, self.view.frame.width, self.view.frame.height))
scrollView.delegate = self
scrollView.pagingEnabled = true
self.greenViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Green View Controller") as! GreenViewController
self.blueViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Blue View Controller") as! BlueViewController
greenViewController.view.frame = CGRectMake(0, 0, view.bounds.width, view.bounds.height)
blueViewController = CGRectMake(0, view.bounds.height, view.bounds.width, view.bounds.height)
scrollView.addSubview(greenViewController.view)
scrollView.addSubview(blueViewController.view)
scrollView.contentSize = CGSizeMake(view.bounds.width, view.bounds.height*2)
self.view.addSubview(scrollView)
}
I hope that I have expressed myself clearly.
EDIT:
I've tried changing the size of the scrollview, when it scrolls. The idea was to change the height of the frame so it matches the height of the textfield when it is scrolled all the way down. But it seems that it also changes the visible part of whats embedded in the scrollview:
func scrollViewDidScroll(scrollView: UIScrollView) {
if self.scrollView.contentOffset.y > textField.View.bounds.height {
self.scrollView.frame.size.height = view.bounds.height - scrollView.contentOffset.y - textField.View.bounds.height
println(self.scrollView.frame)
}
}
Ok it might be bit late now but still i m posting this as a tutorial !
This is a less prefered way to achieve this. Better way would be,
Using table view controller as parent view and then using prototype cells as static cell(In 'n' numbers as per your requirement) as a card view or for any other use
The every different cell used would be considered as a section and no of prototype cells will be equal to no of sections in code as in snippet below
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 2 {
return list.count
}
return 1
}
number of rows in case of section 0 and 1 would be 1 as static part
Whereas No of rows in case of section 2 i.e dynamic part would be equal to count of list.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : CustomTableViewCell.swift = CustomTableViewCell.swift()
switch indexPath.section {
case 0:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell1", forIndexPath: indexPath) as! CustomTableViewCell
break
case 1:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell2", forIndexPath: indexPath) as! CustomTableViewCell
break
case 2:
cell = tableView.dequeueReusableCellWithIdentifier("dynamicCell", forIndexPath: indexPath) as! CustomTableViewCell
break
default:
break
}
return cell;
}
and thats it! Work is done! Party!
I got reference from here
Mixing static and dynamic sections in a grouped table view?
I mocked this up. My View hierarchy looks like this
ViewController's UIView
...UIView (to act as a container view for all the scrollable content)
.......UIView (for the top content) - green
.......UIView (for the bottom content) - blue
............UILabel
............UITableView (with scrollable content - more rows than visible)
I wired #IBOutlets to the UIScrollView (scrollView) and UIView (containerView) for the scrollable area.
in viewDidLoad I added:
scrollView.contentSize = containerView.frame.size
If I click anywhere outside the tableView (top area, text area, etc...) I scrolls the scrollView. If I try to scroll in the table view, the tableView scrolls (the scrollView does not).
Is that what you were trying to achieve?

Resources