I currently have a double for-loop that creates an X by Y grid of UIView CGRect squares. The loop also adds each UIView/Square of the grid to a 2D array allowing me to access each Cell of the grid and alter color/positioning by the index values of the loop.
The loop seems to work fine and displays the Cells/Squares perfectly, however after a while I want to remove all of the squares and also empty the array (but not entirely delete) to make room for a new next grid (which may be of a different size). I created a function to remove all the views from the superview.
This is how I am creating each "Cell" of the grid and placing each into the 2D array:
let xStart = Int(size.width/2/2) - ((gridSizeX * gridScale)/2)
let yStart = Int(size.height/2/2) - ((gridSizeY * gridScale)/2)
let cell : UIView!
cell = UIView(frame: CGRect(x: xStart + (xPos * gridScale), y:yStart + (yPos * gridScale), width:gridScale, height:gridScale))
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor(red:0.00, green:0.00, blue:0.00, alpha:0.02).cgColor
cell.backgroundColor = UIColor(red:1.00, green:1.00, blue:1.00, alpha:0)
cell.tag = 100
self.view?.addSubview(cell)
gridArray[xPos][yPos] = cell
The 2D array is being created on application load like so:
gridArray = Array(repeating: Array(repeating: nil, count: gridSizeY), count: gridSizeX)
I have tried to search for a solution to remove the UIViews from the superview however all answers from other Questions don't seem to work for me. I have tried to add cell.tag = 100 to each UIView and then remove all UIViews with the Tag Value of 100 like:
for subview in (self.view?.subviews)! {
if (subview.tag == 100) {
subview.removeFromSuperview()
}
}
However no luck. I have also tried to use this method:
self.view?.subviews.forEach({
$0.removeConstraints($0.constraints)
$0.removeFromSuperview()
})
The main differences I notice in my code compared to other peoples answers is that I have "?" and "!" at places in the code. I researched about what they mean and understood most of it however I still do not know how to fix my code because if it, and feel as though that is the issue. All I know is that the attempts to remove the UIViews from the superview does not work, and without the "?" and "!"s the code doesn't run at all.
How about to create tag for each cell you are using for example:
//Init value for for the tag.
var n = 0
func prepareCell() -> UIView {
let xStart = Int(size.width/2/2) - ((gridSizeX * gridScale)/2)
let yStart = Int(size.height/2/2) - ((gridSizeY * gridScale)/2)
let cell : UIView!
cell = UIView(frame: CGRect(x: xStart + (xPos * gridScale), y:yStart + (yPos * gridScale), width:gridScale, height:gridScale))
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor(red:0.00, green:0.00, blue:0.00, alpha:0.02).cgColor
cell.backgroundColor = UIColor(red:1.00, green:1.00, blue:1.00, alpha:0)
cell.tag = n
//Each cell should have new value
n += 1
return cell
}
And now remove required views.
func removeViews() {
for z in 0...n {
if let viewWithTag = self.view.viewWithTag(z) {
viewWithTag.removeFromSuperview()
}
else {
print("tag not found")
}
}
}
Example that is working in the playground:
var n = 0
let mainView = UIView()
func createView() -> UIView {
let view = UIView()
view.tag = n
n += 1
return view
}
for i in 0...16 {
mainView.addSubview(createView())
}
func removeViews() {
for z in 0...n {
if let viewWithTag = mainView.viewWithTag(z) {
viewWithTag.removeFromSuperview()
print("removed")
}
else {
print("tag not found")
}
}
}
You might be overlooking a much, much simpler way to do this...
You have built a 2D Array containing references to the "cells" as you populated your view. So, just use that Array to remove them.
// when you're ready to remove them
for subArray in gridArray {
for cell in subArray {
cell.removeFromSuperview()
}
}
// clear out the Array
gridArray = Array<Array<UIView>>()
I'm trying to implement a smoother way for a use to add new collectionViewCells for myCollectionView (which only shows one cell at a time). I want it to be something like when the user swipes left and if the user is on the last cell myCollectionView inserts a new cell when the user is swiping so that the user swipes left "into" the cell. And also I only allow the user to scroll one cell at a time.
EDIT:
So I think it is a bit hard to describe it in words so here is a gif to show what I mean
So in the past few weeks I have been trying to implement this in a number of different ways, and the one that I have found the most success with is by using the scrollViewWillEndDragging delegate method and I have implemented it like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Getting the size of the cells
let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width
let cellPadding = 10.0 as! CGFloat
// Calculating which page "card" we should be on
let currentOffset = scrollView.contentOffset.x - cellWidth/2
print("currentOffset is \(currentOffset)")
let cardWidth = cellWidth + cellPadding
var page = Int(round((currentOffset)/(cardWidth) + 1))
print("current page number is: \(page)")
if (velocity.x < 0) {
page -= 1
}
if (velocity.x > 0) {
page += 1
}
print("Updated page number is: \(page)")
print("Previous page number is: \(self.previousPage)")
// Only allowing the user to scroll for one page!
if(page > self.previousPage) {
page = self.previousPage + 1
self.previousPage = page
}
else if (page == self.previousPage) {
page = self.previousPage
}
else {
page = self.previousPage - 1
self.previousPage = page
}
print("new page number is: " + String(page))
print("addedCards.count + 1 is: " + String(addedCards.count + 1))
if (page == addedCards.count) {
print("reloading data")
// Update data source
addedCards.append("card")
// Method 1
cardCollectionView.reloadData()
// Method 2
// let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
// cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
}
// print("Centering on new cell")
// Center the cardCollectionView on the new page
let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
print("newOffset is: \(newOffset)")
targetContentOffset.memory.x = newOffset
}
Although I think I have nearly got the desired result there are still some concerns and also bugs that I have found.
My main concern is the fact that instead of inserting a single cell at the end of myCollectionView I'm reloading the whole table. The reason that I do this is because if I didn't then myCollectionView.contentOffset wouldn't be changed and as a result when a new cell is created, myCollectionView isn't centered on the newly created cell.
1. If the user scrolls very slowly and then stops, the new cell gets created but then myCollectionView gets stuck in between two cells it doesn't center in on the newly created cell.
2. When myCollectionView is in between the second last cell and the last cell as a result of 1., the next time the user swipes right, instead of creating one single cell, two cells are created.
I've also used different ways to implement this behaviour such as using scrollViewDidScroll, and various other but to no avail. Can anyone point me in the right direction as I am kind of lost.
Here is a link to download my project if you want to see the interaction:
My Example Project
These the old methods if you're interested:
To do this I have tried 2 ways,
The first being:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// page is the current cell that the user is on
// addedCards is the array that the data source works with
if (page == addedCards.count + 1) {
let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
addedCards.append(placeholderFlashCardProxy)
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
cardCollectionView.reloadData()
}
}
The problem with using this method is that:
Sometimes I will get a crash as a result of: NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.
When a new collectionCell is added it will sometimes display the input that the user has written from the previous cell (this might have sometimes to do with the dequeuing and re-use of cell, although again, I'm not sure and I would be grateful if someone answered it)
The inserting isn't smooth, I want the user to be able to swipe left at the last cell and "into" a new cell. As in if I am currently on the last cell, swiping left would put me automatically at the new cell, because right now when I swipe left a new cell is created by it doesn't center on the newly created cell
The second method that I am using is:
let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self
Although the swipe gesture very rarely responds, but if I use a tap gesture, myCollectionView always responds which is very weird (I know again this is a question on its own)
My question is which is the better way to implement what I have described above? And if none are good what should I work with to create the desired results, I've been trying to do this for two days now and I was wondering if someone could point me in the right direction. Thanks!
I hope this helps out in some way :)
UPDATE
I updated the code to fix the issue scrolling in either direction.
The updated gist can be found here
New Updated Gist
Old Gist
First I'm gonna define some model to for a Card
class Card {
var someCardData : String?
}
Next, create a collection view cell, with a card view inside that we will apply the transform to
class CollectionViewCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
self.addSubview(cardlabel)
}
override func prepareForReuse() {
super.prepareForReuse()
cardView.alpha = 1.0
cardView.layer.transform = CATransform3DIdentity
}
override func layoutSubviews() {
super.layoutSubviews()
cardView.frame = CGRectMake(contentPadding,
contentPadding,
contentView.bounds.width - (contentPadding * 2.0),
contentView.bounds.height - (contentPadding * 2.0))
cardlabel.frame = cardView.frame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var cardView : UIView = {
[unowned self] in
var view = UIView(frame: CGRectZero)
view.backgroundColor = UIColor.whiteColor()
return view
}()
lazy var cardlabel : UILabel = {
[unowned self] in
var label = UILabel(frame: CGRectZero)
label.backgroundColor = UIColor.whiteColor()
label.textAlignment = .Center
return label
}()
}
Next setup the view controller with a collection view. As you will see there is a CustomCollectionView class, which I will define near the end.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var cards = [Card(), Card()]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding)
}
lazy var collectionView : CollectionView = {
[unowned self] in
// MARK: Custom Flow Layout defined below
var layout = CustomCollectionViewFlowLayout()
layout.contentDelegate = self
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
collectionView.clipsToBounds = true
collectionView.showsVerticalScrollIndicator = false
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cards.count
}
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize {
return CGSizeMake(collectionView.bounds.width, tableViewHeight)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cellIdentifier = "CollectionViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell
cell.contentView.backgroundColor = UIColor.blueColor()
// UPDATE If the cell is not the initial index, and is equal the to animating index
// Prepare it's initial state
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{
cell.cardView.alpha = 0.0
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0)
}
return cell
}
}
UPDATED - Now For the really tricky part. I'm gonna define the CustomCollectionViewFlowLayout. The protocol callback returns the next insert index calculated by the flow layout
protocol CollectionViewFlowLayoutDelegate : class {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath)
}
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var contentDelegate: CollectionViewFlowLayoutDelegate?
// Tracks the card to be animated
// TODO: - Adjusted if cards are deleted by one if cards are deleted
private var animatingIndex : Int = 0
// Tracks thje currently visible index
private var visibleIndex : Int = 0 {
didSet {
if visibleIndex > oldValue {
if visibleIndex > animatingIndex {
// Only increment the animating index forward
animatingIndex = visibleIndex
}
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 {
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0)
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex)
}
} else if visibleIndex < oldValue && animatingIndex == oldValue {
// if we start panning to the left, and the animating index is the old value
// let set the animating index to the last card.
animatingIndex = oldValue + 1
}
}
}
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enought in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}
And define the delegate methods in your view controller to insert a new card when the delegate tell it that it needs a new index
extension ViewController : CollectionViewFlowLayoutDelegate {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
cards.append(Card())
collectionView.performBatchUpdates({
self.collectionView.insertItemsAtIndexPaths([index])
}) { (complete) in
}
}
And below is the custom Collection view that applies the animation while scrolling accordingly :)
class CollectionView : UICollectionView {
override var contentOffset: CGPoint {
didSet {
if self.tracking {
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go
// So you should be adjusting the second to last cell on the screen
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0))
} else {
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index
// by adding 1, thus we need to continue the trasition on the second the last cell
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0))
}
}
}
/**
This method applies the transform accordingly to the cell at a specified index
- parameter atIndex: index of the cell to adjust
*/
func adjustTransitionForOffset(atIndex : NSIndexPath) {
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell {
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width
lastCell.cardView.alpha = progress
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0)
}
}
}
I think u missed one point here in
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
the indexPath should be
NSIndexPath(forItem : addedCards.count - 1, inSection : 0) not addedCards.count
Thats why you were getting the error
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0
i'm making an messenger app, like telegram, i would like to implement an infinite scroll, i use JSQMessageViewController for the chat.
this is the code for infinite scroll:
override func scrollViewDidScroll(scrollView: UIScrollView) {
if(scrollView.contentOffset.y >= 0.0 && scrollView.contentOffset.y <= 30.0 && loading == true){
self.loading = false
self.loadMore()
}
}
Then i call this function:
func loadMore() {
//Controllo se nell'array sono presenti altri messaggi
if ((chatList?.messages.count)!-1) != self.messages.count{
//
CATransaction.begin()
CATransaction.setDisableActions(true)
let oldBottomOffset = self.collectionView!.contentSize.height - self.collectionView!.contentOffset.y
UIView.performWithoutAnimation({ () -> Void in
self.collectionView!.performBatchUpdates({ () -> Void in
let idInit = (self.lastMessageId-self.messagePerPage<0) ? 0 : self.lastMessageId-1-self.messagePerPage
let idEnd = self.lastMessageId-1
var indexPaths: [NSIndexPath] = []
for(var i = 0; i<idEnd-idInit; i++){
print("Creo indexpath \(i)")
indexPaths.append(NSIndexPath(forItem: i, inSection: 0))
}
self.collectionView!.insertItemsAtIndexPaths(indexPaths)
let msg = self.chatList?.messages
var i = idEnd
repeat {
let mg = msg![i]
let messagechat:JSQMessage = JSQMessage(senderId: mg.sender, senderDisplayName: "", date:mg.time, text: mg.message)
messagechat.xmppMessageID = mg.messageID
messagechat.status = mg.messageStatus
self.messages.insert(messagechat, atIndex: 0)
self.lastMessageId = i
i--
} while i > idInit
// invalidate layout
self.collectionView!.collectionViewLayout.invalidateLayoutWithContext(JSQMessagesCollectionViewFlowLayoutInvalidationContext())
}, completion: {(finished) in
//scroll back to current position
self.finishReceivingMessageAnimated(false)
self.collectionView!.layoutIfNeeded()
self.collectionView!.contentOffset = CGPointMake(0, self.collectionView!.contentSize.height - oldBottomOffset)
CATransaction.commit()
})
})
}
else {
print("No more messages to load.")
}
}
All work fine but while scrolling the scroll is stopped for 1sec. i suppose is CATransaction that make this and sometimes i see a jump effect, if i add 10 new messages i see for a second the first message of the 10, and then offset is set correctly to the older position.
How can i fix this? i see telegram scrolling is perfect, and there is no jump effects while loading old messages.
I am trying to build a UICollectionViewas tag flow layout according to this topic: http://codentrick.com/create-a-tag-flow-layout-with-uicollectionview/
It worked until the button action but there is one issue. As you can see at the picture below, custom UICollectionViewCell's don't align correctly as like as at the topic. I did everything same but FlowLayout.swift
I want to make equal gaps between these cells. But they are aligning like this.
You can find FlowLayout.swift below.
Can someone tell me how can I fix it?
import Foundation
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElementsInRect(rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
var leftMargin : CGFloat = 0.0
for attributes in attributesForElementsInRect! {
let refAttributes = attributes
// assign value if next row
if (refAttributes.frame.origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
// set x position of attributes to current margin
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = leftMargin
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current Fmargin
leftMargin += refAttributes.frame.size.width + 8
newAttributesForElementsInRect.append(refAttributes)
}
return newAttributesForElementsInRect
}
}
I know this is an old thread but the problem still persists. The code here helped me, to solve my problem but I created a more swifty and robust version of it. Already tested with Swift 5.1 and iOS 13.
// A collection view flow layout in which all items get left aligned
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let originalAttributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var leftMargin: CGFloat = 0.0
var lastY: Int = 0
return originalAttributes.map {
let changedAttribute = $0
// Check if start of a new row.
// Center Y should be equal for all items on the same row
if Int(changedAttribute.center.y.rounded()) != lastY {
leftMargin = sectionInset.left
}
changedAttribute.frame.origin.x = leftMargin
lastY = Int(changedAttribute.center.y.rounded())
leftMargin += changedAttribute.frame.width + minimumInteritemSpacing
return changedAttribute
}
}
}
Solved.
I changed the UICollectionView's layout flow to custom and set FlowLayout.swift as UICollectionView's class.
I am trying to populate a two dimensional array with a custom class called "card". I run it through two for loops to make a new "card" object and append it to the array. Every time I run the code block, I get differing amounts of objects ranging from 0 to millions.
class arcade: UIViewController {
override func viewDidLoad() {
createBoard()
displayBoard()
}
func createBoard()
{
var repCard = card()
var board = [[card](count: 0, repeatedValue: repCard)]
var indexCounter = 0;
for var xInFor = 0; xInFor < 5; xInFor++ {
for var yInFor = 0; yInFor < 5; yInFor++ {
var newCard = card()
newCard.width = 15.0
newCard.id = indexCounter
++indexCounter
var xPos = 50
var yPos = 50
newCard.x = xPos * xInFor
newCard.y = yPos * yInFor
board[xInFor].append(newCard)
}
board.append([card](count: 0, repeatedValue: repCard))
}
}
Problem solved - I needed to declare my "board" array in the class not in the method