Use UICollectionViews to create dynamic and multiple features - ios

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" }
}
}
}

Related

UICollectionView with full page cells VS UIScrollView with child view controllers

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()
}
}
}

Table cell only updates when I select it

I'm making an iOS app which uses a table, and each cell contains two labels and a custom view I made called ColourCircle. ColourCircle has a property called fill: UIColor and I've overridden the draw(_ rect: CGRect) function to draw a circle of that colour.
The cells' data are loaded from an array: data: [(String, Double)], using the view controller as both the UITableViewDelegate and UITableViewDataSource.
In viewWillAppear I've written:
guard let fetchedData = getArtistData() else {
performSegue(withIdentifier: "data-error", sender: nil)
return
}
chart.load(data: fetchedData)
data = fetchedData
table.reloadData()
table.setNeedsLayout()
table.isHidden = false
chart.isHidden = false
activityIndicator.stopAnimating()
And, in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) I've written:
let cell = table.dequeueReusableCell(withIdentifier: "artist cell") as! ArtistCell
let name = data[indexPath.row].0
if let proportion = chart.getPercentage(of: name) {
let percentage = proportion * 100
cell.name.text = name
cell.percentage.text = "\(round(percentage * 10) / 10)%" // rounded to 2 d.p.
cell.colour.fill = chart.colour(for: name)!
}
return cell
When I run my app, the visible cells (i.e. which are on the screen) work perfectly, and the ColourCircle views display the correct colour. When I scroll down, bringing new cells into view, the new cells don't have the correct colour, but instead use the colours from the previously loaded cells, in order.
For example, say the first two cells in the table are red and blue, then the first two which load when I scroll down will also be red and blue, in that order. The same thing happens when I scroll up, re-loading the previous cells.
When I select these cells, however, they change to the correct colour. Then, when unloaded and loaded again, they will stay the correct colour unless I've fixed one of the cells further up (by selecting them), in which case they will change to the colours of the ones I fixed.
It seems like it's something to do with reusing the cells, although I can't quite work out how to fix it.
EDIT: Here's the code for my ArtistCell class:
class ArtistCell: UITableViewCell {
#IBOutlet weak var colour: ColourCircle!
#IBOutlet weak var name: UILabel!
#IBOutlet weak var percentage: UILabel!
}
according this: let cell = table.dequeueReusableCell(withIdentifier: "artist cell") as! ArtistCell - all cells are reusable. Use prepareForReuse() method, to prepare cell to reuse and clean up they content.

how to hide the image/label in StoryBoard to show up when we run the app?

I am making a table view app which retrieves the data from the Firebase. when making the user interface in the storyboard, I am using dummy image and label to visualize my app.
but when i run the app which consists of dynamic table view, those dummy images and label also shows up before immediately replaced by the actual data that i download from the Firebase storage.
can I set those images and labels to not show up when i run the app but still available in the storyboard?
thanks in advance :)
If they're just dummies, you can get rid of them when your view loads, before it appears onscreen:
override func viewDidLoad() -> Void{
super.viewDidLoad()
dummy.removeFromSuperview()
}
Whenever you want to hide/show an UIView:
myView.isHidden = true // hide
myView.isHidden = false // show
I assume what you need is to hide the views in viewWillAppear and then show them when necessary.
In your custom cell class, define something to hide the unwanted views:
func hideDummyViews() {
// do some stuff to hide what you don't want, e.g.
myEnclosingStackView.isHidden = true
}
In your table view controller, in the cellForRowAt indexPath func:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell ..
if yourDataSource[indexPath.row].isCompletelyLoaded {
// do your fancy dynamic cell layout
} else {
// show the basic version (minus any dummy views)
cell.hideDummyViews()
}
return cell
You can choose your preferred method for hiding the items (isHidden for each view, removing, adjusting constraints). I prefer to embed any disappearing views in a stack view and then use isHidden = true on the enclosing stack. This keeps things organized in your storyboard/XIB file and neatly recalculates constraints for the hidden stacks.
It seems that you want to show some empty (or incomplete) cells until database content arrives and then you will reload each cell as you process new entries in the datasource. This answer will initially give you a set of cells appearing as per your storyboard/XIB, minus the hidden dummy elements. Then as items in your datasource are loaded fully, you can reload the cells.
By the way, it seems like a lot of work to carefully layout these dummy views for "visualization" and then never show them in the app. Why not have some user-friendly place holders or progress indicators showing and then animate in the real/dynamic views as the data arrives?
I assume you download the Image from FireBase and does't want the dummy Image to appear
(Why dont you declare an empty ImageView and an empty Label!). Try setting an Image array in the viewController. Or you can use a struct array if you want a Image and label text together.
var arrImage=[UIImage]()
var arrLblTxt=[String]()
In view did load append your Demo Image's if required.
arrImage.append(UIImage("Demo"))
arrLblTxt.append("Demo")
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tbl_AssignEmployees.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as YourCell
if arrImage[IndexPath.row] != UIImage("Demo")
{
cell.ImageView.Image = arrImage[indexPath.row]
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
else
{
cell.ImageView.Image = nil
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsetsZero
cell.layoutMargins = UIEdgeInsetsZero
return cell
}
Then After you download your Image From FireBase,
Reset your Image array with the new Images downloaded from fireBase
Then reload your tableView. tableView.reloadData

How can I identify a table row in iOS?

I made a prototype cell and I have data from a database to load into the cells in a table.
Each cell has a label and 3 buttons just like this:
If I click on a button for example "Meets Standard", how can I identify in which row I tapped the button?
So for example when I press the "Meets Standard" button at a given row I'd like to change the background color of that row to red. How can I do it?
I have a CustomCell.swift class where I configure the prototype cell and a TableView.swift class where I configure the table.
Try control dragging the button to your CustomCell.swift class and typing a name and selecting "Action" on the popup menu. Then, inside the generated method, you can call self.backgroundColor = UIColor.redColor() or perform any other operations that you'd like.
Edit:
Here's what I think you should do to change the height:
Make a boolean in your cell class called "expanded" or something. Then, go into your table view class and implement the heightForRowAtIndexPath method. In that method, retrieve the cell and check if it's expanded, and if so, return a larger height. Now, to make it reload, you will need to store a reference to the table view in each cell, as it says here: Reference from UITableViewCell to parent UITableView?
In that clicked method you already made, where the background is set to red, you will need to call:
tableView.beginUpdates()
tableView.endUpdates()
where tableview is the weak reference you already stored. Also, in that method, you need to add self.expanded = true of course.
Edit 2:
You know what, maybe it's easier to just do this:
weak var _tableView: UITableView!
...
func tableView() -> UITableView! {
if _tableView != nil {
return _tableView
}
var view = self.superview
while view != nil && !(view?.isKindOfClass(UITableView))! {
view = view?.superview
}
self._tableView = view as! UITableView
return _tableView
}

Drag & Drop between two UICollectionViews

I need to drag a Cell from CollectionView One and drop it to
CollectionView Two.
The Drag & Drop inside one CollectionView is no Problem, but how
can i get the Cell out of CollectionView One to CollectionView Two?
Any ideas? Any projects or frameworks that have already solved this problem?
Thanks for helping!
https://github.com/Ice3SteveFortune/i3-dragndrop Check this out - its a helper I'm working on to achieve just that. It also supports tableviews
UPDATE
I've recently released the second version of this codebase, called BetweenKit. Its now a fully-fledged drag-and-drop framework.
Hope it proves useful !
When you select the cell from the first collection view, remove it from this collection view, create a new view as copy of that cell place it as subview of the superview on top of all views. Make that view movable using pan gestures. As soon as you "drop" this intermediary cell, detect its position and add it to the current collection view.
Ok, here is the simplest flow ever for the following example:
Add UIGestureRecognizer for every of UICollectionView.
Connect every gesture recognizer with one method:
#IBAction func longPressGestureChanged(recognizer: UILongPressGestureRecognizer) { ... }
Within UIViewController add #IBOutlet for each of UICollectionView:
#IBOutlet var collectionViewGreen: UICollectionView!
#IBOutlet var collectionViewYellow: UICollectionView!
Implement gesture recognizer method to detect changes:
#IBAction func longPressGestureChanged(recognizer: UILongPressGestureRecognizer) {
let globalLocation = recognizer.locationInView(view)
if CGRectContainsPoint(collectionViewGreen.frame, globalLocation) {
//you cover green collection view
let point = view.convertPoint(globalLocation, toView: collectionViewGreen)
if let indexPath = collectionViewGreen.indexPathForItemAtPoint(point) {
//you cover cell in green collection view
} else {
//you do not cover any of cells in green collection view
}
} else if CGRectContainsPoint(collectionViewYellow.frame, globalLocation) {
//you cover yellow collection view
let point = view.convertPoint(globalLocation, toView: collectionViewYellow)
if let indexPath = collectionViewYellow.indexPathForItemAtPoint(point) {
//you cover cell in yellow collection view
} else {
//you do not cover any of cells in yellow collection view
}
} else {
//you do not cover any of collection views
}
}

Resources