UICollectionView to read from left to right with multiple rows - ios

I am creating a UICollectionView that has paging enabled and scrolls from left to right. I am using an NSFetchedResultsController to retrieve results from CoreData, which means it uses collection view sections rather than rows. The collection view has 2 rows and therefore appears like the following screenshot (where the order goes top row, bottom row, top row, bottom row etc):
However, I need the collection view to read from left to right like the following 2 screen shots:
Could you please advise on how I would do this in Swift?

What I would do is this:
1) Sort your data the same way you do it now so that your collection view would look like this:
---------
|0|2|4|6|
---------
|1|3|5|7|
---------
2) In your cellForIndexPath method of your collection view data source return the correct element so that your collection view turns into this:
---------
|0|1|2|3|
---------
|4|5|6|7|
---------
To do that you can use this logic:
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var elementIndex: Int!
if indexPath.row % 2 == 0 {
// 0 / 2 = 0
// 2 / 2 = 1
// 4 / 2 = 2, etc
elementIndex = indexPath.row / 2
}
else {
// (1 / 2) + round(8.0 / 2.0) = 4
// (3 / 2) + round(8.0 / 2.0) = 5
// (5 / 2) + round(8.0 / 2.0) = 6, etc
elementIndex = (indexPath.row / 2) + Int(round(Double(elements.count) / 2.0))
}
// Dequeue your cell
// let cell = ...
cell.element = elements[elementIndex]
return cell
}
Basically this should return your elements in the correct order.

First, I think it could help if you provide an example how your cells are sectioned currently (e.g. section 0: a, b, c, d, section 1: e, f, ...) and how you want the different sections to be displayed/layouted.
I'd suggest to create a custom UICollectionViewLayout subclass.
In the prepareLayout method you should calculate and cache the UICollectionViewLayoutAttributes for each cell.
Create variables:
let xOffset = 0.0 // leftContentInset within collectionView.bounds
let yOffsetRow0 = 0.0 // y offset of first row within collectionView.bounds
let yOffsetRow1 = 50.0 // y offset of second row within collectionView.bounds
var currentXOffset : CGFloat = xOffset
var currentPage : Int = 0
calculate the frames until first row is filled to right screen edge (CGRect(currentXOffset, yOffsetRow0, cellWidth, cellHeight), increase currentXOffset)
reset currentXOffset = xOffset
repeat 1. with yOffsetRow1 for second row
increment currentPage += 1
reset currentXOffset = xOffset + (collectionView.frame.width * currentPage) // for next page
repeat steps 1-5 until layout for all cells is calculated

As you said, you need
1. Paging enabled
2. Scrolling from left to right
3. Multiple sections.
It's a little bit confusing what exactly you want to design. It will be much better if can share proper UI screenshot.
As I understand your problem.
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
Check have you assign the horizontal layout to your collectionView.

Related

How to determine when the nth element has been added to reload the table view row in Swift?

I have a UITableViewCell that contains a UICollectionView. In the UICollectionView, each "row" can fit a total of 4 UIImages. I'm having a bit of a difficult time figuring out the logic behind how to determine when the nth element is added to produce the "next" row so I can reload the table view row to expand its UITableViewCell height.
For example, here's how the first row in the UITableViewCell looks like with 4 images:
[ x x x x]
In my code, when the 5th image is added, the UITableViewCell's height expands to fit the additional 5th image:
[ x x x x]
[ 5 ]
It continues on with the 5th, 9th, 13th, 17th, etc. image that I would like to check before reloading the table view row to expand the height of the UITableViewCell to fit the next set of images.
The images are added to an array and currently I'm doing something like this:
if (images.count % 5) == 0
{
self.imagesTableView.reloadRows(at: [ IndexPath(row: 0, section: 4) ],
with: UITableViewRowAnimation.none)
}
Obviously, the logic only works when it is a multiple of 5. I feel like it's a simple logic check, but I cannot for the life of me figure it out.
Can someone point me in the right direction?
Thanks!
The correct condition for your case is
let isMultiple = ((i-1) % n) == 0
Or
let isMultiple = (i % n) == 1
An idea of implementation would be something like:
let itemsPerRow = 4
let filter = { (itemNumber: Int) -> Bool in
return (itemNumber > itemsPerRow) && ((itemNumber % itemsPerRow) == 1)
}
print(Array(1...20).filter(filter))
// prints [5, 9, 13, 17]
I might be totally missing the point of your question but...
I think it should be:
if (images.count % 4) == 1
{
self.imagesTableView.reloadRows(at: [ IndexPath(row: 0, section: 4) ],
with: UITableViewRowAnimation.none)
}
If you want you can also check that (images.count > 5) in case there's a problem with the code running on the first added image.
It looks like you're going to want something along the lines of this:
let previousCount = self.images.count
self.images.append(newElement) // This is where you are updating your images array
let previousNumberOfRows = (previousCount / 4) + 1 // May want to explicitly check the case where previousCount == 0 if you don't want any rows to show for that case
let updatedNumberOfRows = (self.images.count / 4) + 1 // Again, may want to handle 0 items case
let rowDiff = updatedNumberOfRows - previousNumberOfRows
guard rowDiff != 0 else {
return
}
let range: CountableRange
if rowDiff > 0 {
range = (0 ..< rowDiff)
} else {
range = (rowDiff ..< 0)
}
let indexPathsToReload = range.flatMap { sectionIndex in
return IndexPath(row: sectionIndex + previousNumberOfSections, section: 0)
}
self.imagesTableView.reloadRows(at: indexPathsToReload)
I had to type this answer quickly so there's potentially an off-by-one error. Let me know how it goes!

Swift: How do I arrange programatically added checkboxes?

I'm creating an iOS app using Swift, one with checkboxes. Currently, I've placed them inside a view (constrained and all), in the hopes that they would stay there and not mess up the rest of my app. Here's my code so far:
// UI
let lCheckboxHeight: CGFloat = 44.0;
let lCheckboxWidth: CGFloat = 180.0;
let waterSampleTreatmentTitles = ["i - Untreated", "ii - Acidified", "iii - Airfree", "iv - Filtered, Untreated","v - Filtered, Acidified","Stable Isotopes","Others"];
let lNumberOfCheckboxes = waterSampleTreatmentTitles.count
var lFrame = CGRectMake(0, 0, self.view.frame.size.width, lCheckboxHeight);
for (var counter = 0; counter < lNumberOfCheckboxes; counter++) {
let lCheckbox = Checkbox(frame: lFrame, title: waterSampleTreatmentTitles[counter], selected: false);
lCheckbox.mDelegate = self;
lCheckbox.tag = counter;
if (waterSampleTreatmentTitles[counter] == "i - Untreated" && self.flagUntreated == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "ii - Acidified" && self.flagAcidified == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iii - Airfree" && self.flagAirfree == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iv - Filtered, Untreated" && self.flagFilterUntreat == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "v - Filtered, Acidified" && self.flagFilterAcid == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Stable Isotopes" && self.flagStabIso == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Others" && self.flagOthers == true){
lCheckbox.selected = true
}
self.chemistryProductionWell.viewSampTreat.addSubview(lCheckbox);
lFrame.origin.y += lFrame.size.height;
}
Currently, this creates a list of checkboxes, which spills past the view I made, and just generally makes a mess of the app. The view is long enough for maybe two checkboxes vertically, but not eight.
How do I make it such that the checkboxes are arranged horizontally? I've tried replacing the following code:
lFrame.origin.y += lFrame.size.height;
With this:
lFrame.origin.x += lCheckboxWidth
But that doesn't take into account that the text for the checkboxes aren't the same length, and of course ignores the width restriction as well.
How do I make it such that if the checkbox length exceeds the view, it would drop down to the next line?
Thanks.
If the checkbox has no way to determine its own intrinsic size, you'll have to determine the width that each title would take, then factor that value into that checkbox's frame width.
However, wrapping a row of checkboxes will lead to a couple issues. First, the layout would need to be updated upon autorotation. Second, each row's checkboxes would not be vertically aligned with the previous row's checkboxes.
What you preferably want is a checkbox that can determine its own intrinsic size. Now you'd be able to take advantage of the Auto Layout system, and both the checkboxes, and their containing view could size themselves. This would have avoided the problem where the checkboxes are located outside their container's bounds (as well as having to hard code frames).
At that point, you could benefit from an easier solution like UIStackView, instead of having to code their layout on your own.
If you are using Storyboard, the ideal option would be be a checkbox that could not only self-size, but supported IBDesignable. You could then just setup outlets to IB checkboxes, instead of doing any of this in code.
If you aren't able to find a better control, then the easiest way to do what you ask would be as follows.
let lCheckboxHeight: CGFloat = 44.0
// Make the width as wide as necessary to accommodate the largest title
let lCheckboxWidth: CGFloat = 180.0
// Spacing between checkboxes or rows
let xSpacing: CGFloat = 10
let ySpacing: CGFloat = 10
// Current offset for next checkbox
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for counter in 0..<lNumberOfCheckboxes {
var lFrame = CGRect(x: xOffset, y: yOffset, width: lCheckboxWidth, height: lCheckboxHeight);
// Advance offset for upcoming checkbox
xOffset += lCheckboxWidth + xSpacing
// Determine if there is enough room for another horizontal checkbox
let maxX = xOffset + lCheckboxWidth + xSpacing
if maxX >= chemistryProductionWell.viewSampTreat.bounds.maxX {
// Move to start of next row
xOffset = 0
yOffset += lCheckboxHeight + ySpacing
}
// ... Other code here
}
This still doesn't do anything to support autorotation, which is why you should look into an Auto Layout/Adaptive UI approach, and not directly work with frames.
As an aside, although you placed them in a viewSampTreat container, you had based their width on something other than their container, i.e. self.view.frame.size.width. This would have led to a clipping problem.

How to find the maximum possible number of items could be placed in a UICollectionView row?

How to find the maximum possible number of items could be placed in a UICollectionViewrow?
In the following example which is 3.
Apple's UICollectionViewFlowLayout doesn't "know" the max number of items it can display on the screen. It basically just calculates the coordinates of the next cell to place it next to the previous one and verifies if it fits in the screen on the fly. If it doesn't, it will recalculate new coordinates to place the cell as first element of the next line/column (depending on the orientation).
You could do the same with a formula. Something like this maybe :
int verticalCount = (int)((self.collectionViewContentSize.width - self.minimumInteritemSpacing)/(self.itemSize.width + self.minimumInteritemSpacing));
int horizontalCount = (int)((self.collectionViewContentSize.height - self.minimumLineSpacing)/(self.itemSize.height + self.minimumLineSpacing));
use this fomula
int noOfCell = (self.view.frame.size.width - leftPadding - rightPadding)/(cell.frame.size.width + spacebetweenCells);
After considering the answer form #Kujey and with a slide change I could find the solution as follows.
func maximumNumberOfCellsInARow() -> Int {
let collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
return Int((collectionView.contentSize.width - collectionViewFlowLayout.minimumInteritemSpacing)/(collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumInteritemSpacing))
}
#Kujey, sorry I could not mark your answer as a right answer even though my answer is driven from yours, because it needs a significant change.
I have created this formula, you can complicate it including section insets and other params.
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSUInteger totalItems = X;
UICollectionViewFlowLayout* collectionViewLayout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
NSLog(#"MAX WIDTH: %li",(long) collectionView.frame.size.width);
NSUInteger maxVisibleItems = (collectionView.frame.size.width + collectionViewLayout.minimumInteritemSpacing) / (collectionViewLayout.itemSize.width + collectionViewLayout.minimumInteritemSpacing);
NSLog(#"MAX VISIBLE ITEMS: %li",(long) maxVisibleItems);
if (totalItems < maxVisibleItems ) {
return totalItems;
}else{
return maxVisibleItems;
}
}

UICollectionView dynamic data source items adding to the beginning of the collection view

I'm making collection view with a dynamic content loading ability. In my case, I'm making calendar. Current date will be in the visible part (after loading). If you scroll up, the older dates will shown and if you scroll down you'll see next dates.
My collection view use standard flow layout. I have 7 cells in a row and 4 visible rows.
I have a problem with adding older dates (e.g. cells that appears if you scroll up). What I'm doing: first I implement scroll view method for detecting scroll event.
startOffsetY is contentOffset.y value set in viewDidLoad. It's not equal to 0 because I set contentInset. upd is just a flag that means that new update is could be start.
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y <= startOffsetY - 20 && upd {
calendar.updateDataSource()
}
}
Next, I calculate dates for previous time slice (about 4 weeks, I also tried 1 week) from the first date that is in my data source array.
After that, I calculate indexPath array and make update to collection view:
var indexes = [NSIndexPath]()
for n in 1...4 {
for i in reverse(1...7) {
indexes.append(NSIndexPath(forItem: n * 7 - i, inSection: 0))
}
}
self.calendarCollectionView.performBatchUpdates({ () -> Void in
self.calendarCollectionView.insertItemsAtIndexPaths(indexes)
}, completion: { (finish) -> Void in
self.upd = true
})
but, I have visible lags when rows added and scrolling is in progress.
I tried different strategies: I used reloadData() and it was ideal (on the simulator) and extremely laggy on my iPhone 4S (this was the first time in my experience when simulator was faster than the device). From this point, I figure out, that animation of inserting items might be the problem. I tried to wrap performBatchUpdates into the UIView.performWithoutAnimation block, but with no luck also.
I'm really looking for some help, I don't looking for a ready made solutions except they work as I describe (scroll up'n'down and load content) and I can look how it works. Once again, scrolling already loaded items is not a problem, the problem is a lagging when content is add at the begging of my data array.
EDIT
Code provided by #teamnorge in Swift
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
var offset : CGFloat!
println(currentOffset)
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffset < contentHeight * 0.125 {
offset = contentHeight * 0.125 - currentOffset
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset + contentHeight * 0.5 + offset - CGFloat(cellHeight))
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffset > contentHeight * 0.75 {
offset = currentOffset - contentHeight * 0.75
// reload content here
scrollView.contentOffset = CGPoint(x: 0, y: currentOffset - contentHeight * 0.5 - offset + CGFloat(cellHeight))
}
It works very nice, but need to play with contentOffset y formula because right now trick works only when you scroll up. Will fix this tomorrow and add calendar date calculations.
EDIT 2
reloading data ruins everything in all of my prototypes. Including the one I made previously. Found something that makes lags a bit lower but still very noticeable and totally unacceptable. Here are these things:
remove autolayout from cell prototype in the storyboard
add this code to the cell:
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.mainScreen().scale
I think that invalidation of the layout results in these lags. So, I maybe I need to make custom flow layout? And if so what recommendations can you give.
That's actually the very interesting topic. I'll try to explain the idea I came up with while working on another Calendar app.
In my case it was not a Calendar View but Day View, hours 00:00 - 24:00 listed from top to bottom, so I had UITableView and not UICollectionView but it's not that important in this case.
The language used was not Swift but Objective-C, so I just try to translate code samples.
Instead of manipulation with the data source I created UITableView with the fixed amount of rows, in my case to store exactly two days (48 rows, two days of 24 hours each). You could choose the amount of rows containing in two full screens.
The important thing is that total amount of rows must be a multiple of 8.
Then you need a formula to calculate what's the day number for each particular cell based on what's inside the first visible row.
The idea is that UICollectionView is in fact UIScrollView so when we scroll down or up we can handle the corresponding event and calculate the visible offsets.
To simulate the infinitive scrolling we handle the scrollViewDidScroll and check if you just passed the 1/8 of the UIScrollView height scrolling up, move your UIScrollView to the 1/2 of height plus the exact offset so it moves to the "second screen" smoothly. And back, if you passed the 6/8 of the UIScrollView height while scrolling down, move the UIScrollView up to 1/2 of its height minus offset.
When you do this you see scrolling indicator jumps up and down which is very confusing so we have to hide it, just put somewhere in viewDidLoad:
calendarView.showsHorizontalScrollIndicator = false
calendarView.showsVerticalScrollIndicator = false
where calendarView is your UICollectionView instance name.
Here is the code (translated from Objective-C right here, not tried in the real project):
func scrollViewDidScroll(scrollView_:UIScrollView) {
if !enableScrollingHandling {return}
var currentOffsetX:CGFloat = scrollView_.contentOffset.x
var currentOffSetY:CGFloat = scrollView_.contentOffset.y
var contentHeight:CGFloat = scrollView_.contentSize.height
var offset:CGFloat
/* 0.125 - 1/8, 0.5 - 1/2 */
if currentOffSetY < (contentHeight * 0.125) {
enableScrollingHandling = false
offset = (contentHeight * 0.125) - currentOffSetY
// #todo: your code, specify which days are listed in the first row (2nd screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset +
contentHeight * 0.5 + offset - CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
/* 0.75 - 6/8, 0.5 - 1/2 */
if currentOffSetY > (contentHeight * 0.75) {
enableScrollingHandling = false
offset = currentOffSetY - (contentHeight * 0.75)
// #todo: your code, specify which days are listed in the first row (1st screen)
scrollView_.contentOffset = CGPoint(x: currentOffsetX, y: currentOffset -
contentHeight * 0.5 - offset + CGFloat(kRowHeight))
enableScrollingHandling = true
return
}
}
Then in your collectionView:cellForItemAtIndexPath: based on what's in the first row (is this content of the first screen or second screen) and what are the current visible days (formula) you just update the cell content.
This formula could also be a tricky part and may require some workaround especially if you decide to put Month Names in between months. I do also have some ideas on how to organise it, so if you encounter any problem we can discuss it here.
But such an approach to simulate infinitive scrolling with "two screens loop and jumps in between" works really like a charm, very smooth scrolling like behaviour tested on older phones also.
PS: kRowHeight is just a constant the height of the exact row (cell) it's needed for precise and smooth scrolling behaviour it could be skipped I think.
UPDATE:
Important notice, I skipped this from original code, but see this is important. When you manually set the contentOffset you also triggers the scrollViewDidScroll event, to prevent this you need to temporary disable your scrollViewDidScroll event processing. You can do it by adding, for example, state variable enableScrollHandling and change its state true/false, see updated code.

iOS : Adaptive Collection View cell width

I'm trying to make a grid view on iOS with adaptive column width.
On Android it's possible to do this by setting stretchMode attribute to spacingWidth .
This attribute take the cell width as minimum width and grow cell automatically if there is free space available but no enough to add another column keeping the same space between column everywhere.
I didn't found any way to do that on iOS.
It look like that (left image) on iPhone 6, but on iPhone 5 (right image) the space is very big and ugly. I wan't to auto resize cells to avoid this big space.
How can i do that on iOS ?
(I'm using Xcode 6.1)
Thanks
EDIT :
This i why i wan't (black space is approximatively desired additional cell width, sorry my draw is ugly I did it quickly)
EDIT 2:
I tried to calculate new size with this code, but the result is "strange" (wrong size, position) , i think i missed something
let CELL_SIZE : Float = 92
let CELL_MARGIN : Float = 10
let COLLECTION_VIEW_MARGIN : Float = 20 //Left right margin
let screenWidth = Float(UIScreen.mainScreen().bounds.width)
let numberOfCell = Float(Int(screenWidth / (CELL_SIZE + CELL_MARGIN + COLLECTION_VIEW_MARGIN)))
let oldCellSize = Float(cell.frame.width)
var newCellSize : Float
if(numberOfCell >= 2){
newCellSize = (screenWidth / numberOfCell) - (CELL_MARGIN * (numberOfCell-1))
} else {
newCellSize = (screenWidth / numberOfCell) }
let indexPathRow = Float(indexPath.row)
var xOffsetMultiplier = indexPathRow % numberOfCell
if(xOffsetMultiplier == 0){
xOffsetMultiplier = numberOfCell
}
var newX : Float = 0
if(xOffsetMultiplier == 1){
newX = COLLECTION_VIEW_MARGIN / 2
} else {
newX = (newCellSize + CELL_MARGIN) * (xOffsetMultiplier-1) + (COLLECTION_VIEW_MARGIN / 2)
}
var frame = CGRectMake(CGFloat(newX), cell.frame.minY, CGFloat(newCellSize), cell.frame.height)
cell.frame = frame
This code is written in func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell in my ViewController
You should use UICollectionViewFlowLayout to adopt cell size according to collection view size.

Resources