CollectionView.cellForItem returning nil - swift

I've created a collectionView in Storyboard and then put the Delegate and DataSource methods in an extension to the ViewController which manages that screen.
The collectionView uses a layoutDelegate to show a four-by-four grid of images. All cells are shown in the grid, so a cell not being visible isn't a problem and they are all instances of the class imageCVC, a subclass of UICollectionViewCell
This all loads without a problem, but I now want to manipulate four random images before passing control to the user. Mindful that the collectionView may not have fully loaded by the end of viewDidLoad, I call the routine that chooses which image to manipulate, changeImages() in the viewDidLayoutSubviews method. The function is as follows:
func changeImages() {
collectionView.layoutIfNeeded()
let maxChanges = 30
var imageIndex = 0
var imageChanges 0
while imageChanges < maxChanges {
imageIndex = Int.random(in: 0..<(collectionView.numberOfItems(inSection: 0)))
if let cell = collectionView.cellForItem(as: IndexPath(row: imageIndex, section: 0)) as? imageCVC {
changeCell(cell)
imagesChanges += 1
}
}
}
(EDIT: Incorporated Sam's suggestion (below), but it still always returns nil!)
Unfortunately, whilst the imageIndex gets set correctly (so the collection knows how many elements it has), the cellForItem call always returns nil. I've forced the layout at the beginning of the function, but it has no effect.
Please could someone let me know what I'm doing wrong? Many thanks in advance.

In the following line:
imageIndex = Int.random(in: 0...(collectionView.numberOfItems(inSection: 0)))
The code starts from 0 and goes all the way to the collection view items count, so if the count is 10, the code goes from 0 to 10 including 10 which is 11 items in total. This is probably what is causing the crash since there are only 10 items and we try to access 11 items.
Just change:
0...(collectionView.numberOfItems(inSection: 0)
To
0..<(collectionView.numberOfItems(inSection: 0)

After further investigation, it appears that the collectionView data is not being loaded until after the viewDidLayoutSubviews - which seems a little contradictory to me, but hey, I'm sure there's a good reason... - and so I have implemented what I consider to be a work-around.
I've taken the call to changeImages() out from the viewDidLayoutSubviews and put it into the completion segment of a DispatchQueue.main..., written in the viewDidLoad, as follows:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
self.changeImages()
})
Essentially, I'm giving the system time (0.3 seconds) to complete it's full loading of the subviews, rather than actually placing my code at the correct part of the cycle when I know that the views have been fully loaded. A solution but, I suspect, an inelegant one.
If anyone knows how I should be approaching it, I'd be very interested to hear. Thanks.

Related

Swift array bounds error. Carefully constrained index - out of bounds anyway. Why?

Friends
I have some code very similar to the block below.
The line: let item = selectedErrorList[i] is blowing up because of a out of bounds error.
The context is a dialogue with two tabs: One displaying a ScrollView with ~100 elements, the other a ScrollView with about 4 elements. When the user scrolls to the bottom of the big list and then clicks on the tab of the small list which will rerun this code, the error occurs
I am convinced that i is constrained to the indexes of selectedList and when the tab is changed everything should be reinitialised. That would make this a bug in Swift.
Does this ring any bells for anyone? Seen a similar problem?
var selectedErrorList = array of items;
ScrollView(showsIndicators: true){
VStack(spacing: 0) {
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
// Do something with item
}
}
}
shouldn't it be : ForEach(0..<(selectedErrorList.count - 1)) ?
array index starts a zero , ends at number of indexes minus one.
Solved this.
Needed this to maintain the index:
private func adjustIndex(i: inout Int) -> Int {
// This function is needed to increment the index `i`. Because SwiftUI will not let code into a declaration but will let a function call, and `i++` is not a thing, let alone a function call, in Swift.
let ret = i
i += 1
return ret
}
var i = 1
var selectedErrorList = array of items;
ScrollView(showsIndicators: true){
VStack(spacing: 0) {
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
// Do something with item
someThingWithItem(item, adjustIndex(i: &i))
}
}
}
Using the idiom that I was resulted in a warning from Swift (I had not noticed due to clutter in the console)
The adjustIndex function is a tacky hack but gets around the fact that statements in a SwiftUI view are tricky. I am new to SwiftUI and I have not figgured those rules yet, but:
ForEach(0..<selectedErrorList.count){ i in
let item = selectedErrorList[i]
i += 1 // Error here
someThingWithItem(item, i)
}
will not work.
As a interesting aside:
ScrollView(showsIndicators: true){
LazyVStack(spacing: 0) {
resulted in elements being added into the output ScrollView out of order when scrolling and switching between tabs.
I did think that SwiftUI did all =the GUI work in one thread. I misunderstand, but that is another issue.

Dealing with index out of bounds in SWIFT in tableview cell

Language : Swift , REALM, Working with tableview cells to display user entries.
I have a problem where I cannot figure out how to display images in a table view cell where each cell has different number of images coming in and the images itself are optional when the user saves their entry.
here is my problem - When I create a new image view in my stack view as you see in the code, to display the second image in the list, I am getting the error that the index path is out of bounds. Also, I cannot unwrap the journalAspects.inPictures[0].realmToThumbNailImage() because I get an error saying that it is not optional.
How do I go about solving this problem of showing different number of images in different cells with out crashing the app.
I have 3 user entries in my app
1st one has text, date and no images
2nd one has text, date and 1 image
3rd one has text, date and 2 images
here is what I want to see in my table view cell
first cell with just text and date
second cell with text, date and 1 image
third cell with text, date and 2 images
Thank you very much and your input is highly appreciated.
here is my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let displayCell = journalAspectTableViewDispay.dequeueReusableCell(withIdentifier: "firstAddCell", for: indexPath) as! FirstAddTableViewCell
if let journalAspects = RealmEntries?[indexPath.row] {
//MARK: Text display
displayCell.journalTextDisplayLabel.text = journalAspects.realmText
let pictureImageView = UIImageView()
pictureImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
pictureImageView.widthAnchor.constraint(equalToConstant: 70).isActive = truedisplayCell.stackViewForImageShow.addArrangedSubview(pictureImageView)
let secondpictureImageView = UIImageView()
secondpictureImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
secondpictureImageView.widthAnchor.constraint(equalToConstant: 70).isActive = true
displayCell.stackViewForImageShow.addArrangedSubview(secondpictureImageView)
if journalAspects.inPictures.count == 0 {
return displayCell
} else {
let imagesComingOut = journalAspects.inPictures[0].realmToThumbNailImage()
secondpictureImageView.image = imagesComingOut
let secondimagesComingOut = journalAspects.inPictures[1].realmToThumbNailImage() -- App crashes
pictureImageView.image = secondimagesComingOut -- App crashes
}
}
return displayCell
}
Hi, Thank you for your feedback, It is working, But when I implement it, I am having a strange problem. As I add new entries, the number of rows in the section won't change, and weird things happen. At first as I run and compile the app from the Xcode, everything seems right. Entries with one image has one image, entries with no image has no image and entries with 2 has 2. But as I scroll up and down, suddenly entries with no image gets populated with some image. Even though, I have added a new entry, the number of rows will still return the same. For instance if I have 10 entries at the start by running the compiler on the Xcode, then I add the 11th entry, as I scroll, the table view adds the 11th entry as 10th and knocks out the first entry. No matter how many I add, they just knock one down and add another at the top making the number of rows I see as a constant number since it is compiled. If you have any idea as to why that is happening, Please let me know Thank you again.
Have you checked that inPictures actually contains 2 images? You check that there's greater than 0, but not > 1. That can be the only reason for an out of range error in that line. A safer way would be:
switch journalAspects.inPictures.count {
case 2:
secondpictureImageView.image = journalAspects.inPictures[0].realmToThumbNailImage()
pictureImageView.image = journalAspects.inPictures[1].realmToThumbNailImage()
case 1:
pictureImageView.image = journalAspects.inPictures[1].realmToThumbNailImage()
// or maybe the other one - hard to tell as you've mixed up sequencing
default: break
}
return displayCell

Indicate when tableView indexPath is last one

I am implementing showing post part, I want it to do pagination, I got Query part. What I understand is I set limited number of posts when user reached last post that updated in tableView, query fetches more objects by next set limit.
For these, I need something indicates when tableView indexPath is reached at last one or before last one.
I searched some, I think this is good one, but I don't know what does mean.
Get notified when UITableView has finished asking for data?
Could anyone explain me what does mean(how it works) and how to subclass in UITableView's reloadData for this?
fun reloadData() {
print("begin reload")
super.reloadData()
print("end reload")
}
I add my fetching code, I don't think it is working.
var limit = 10
var skip = 0
func fetchAllObjectsFromParse() {
//empty postArray
postsArray = []
//bring data from parse
let query = PFQuery(className: "Posts")
query.limit = limit
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error) -> Void in
if error == nil && objects != nil{
for object in objects! {
self.postsArray.append(object)
}
if (objects!.count == self.limit){
let query = PFQuery(className: "Posts")
self.skip += self.limit
query.skip = self.skip
query.limit = self.limit
print(self.limit)
print(self.skip)
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil && objects != nil {
for object in objects! {
self.postsArray.append(object)
print(objects?.count)
}
}
})
dispatch_async(dispatch_get_main_queue(),{
self.tableView.reloadData()
})
}
}else{
print(error?.localizedDescription)
}
}
}
First let's tackle pagination. Now pagination isn't undoable but it does require your server to accept pages so that it knows which set of results to return. For example if you have 100 results in group sizes of twenty, in your url request you'd have to specify if you wanted page 0 - 4.
I would personally recommend using an open-source library to take care of pagination for you. Take a look at MMRecord, it does have some pagination support for you. If you are feeling ambitious and you want to implement it on your own, you'll need to build some networking service, best built on something like AFNetworking that keeps track of the last requested page.
In swift, that would look something like this:
NetworkManager.GET('http://someURI', parameters: [:], page: 0)
Somehow you have to keep the state of which page is next around. A better design, as suggested here, is to use protocols and some sort of datasource wrapper that abstracts the mechanism from the client.
Now you also asked how to detect when a table view scrolls to the end so you can fire off a network request. One way to do it is simple observe scrollViewDidScroll (UITableView inherits from UIScrollView) and check to see if the contentOffset is >= self.tableView.contentSize - self.tableView.frame.size.height (which will be the content offset of when the table is all the way scrolled). However you want the user of your application to be able to keep scrolling without having to scroll to the bottom, waiting for it to load, then continue scrolling so what I would is fire the request when they've scrolled 3/4 down the page or even halfway if you know the request will take a long time. That way by the time they get to the bottom, it'll already have loaded the data and the user will never know the difference.
EDIT:
I would add a few suggestions for you apart from my answer. 1) Take a look at this: http://artsy.github.io/blog/2015/09/24/mvvm-in-swift/
It talks about the MVVM design pattern which is much better for holding state instead of having state variables in your view controller.
2) your function, fetchAllObjectsFromParse is a bad name for a couple of reasons. First, the name implies that the function fetches all the objects at once, which isn't the case if you want it paginated.
Maybe something like this:
func fetchObjectsForModel(model: String, atPage page: Int, withBlock block: ([PFObejct], NSError?) -> Void {
}
And then make another function which will generate your query:
func generateQueryWithSkips(skip: Int, limit: Int) -> PFQuery {}

Swift 1.2 closure optimisation bug

I have an array of objects which all have a timeStamp (NSDate) and I am trying to sort them with the closure below.
The problem is that this works well without any optimisation, so with debug builds all is fine and dandy. But in with optimisations I get the following EXC_BAD_ACCESS.
reportCells.sort({ (a: UITableViewCell, b: UITableViewCell) -> Bool in
if let first = a as? GreenelyTableViewCell,
second = b as? GreenelyTableViewCell {
if let firstDate = first.timeStamp,
secondDate = second.timeStamp {
let comparison = firstDate.compare(secondDate)
if comparison == NSComparisonResult.OrderedAscending {
return false
} else {
return true
}
}
}
return false
})
Any workarounds?
The issue is you are sorting the cells. Which is not a proper approach first of all. You should sort your data source array. Because table is managing its cells in memory and when you make an array of cells. It will cause issues in backend even if it seems working. Now coming to your problem,
You are sorting cells and when you get back to the tableview listing methods, it agains fetches the data from data source i mean your array of objects. And tableview don't finds proper arrangements of data. I mean your cells are in 1 , 4, 2, 3 order after your sort while your data is stil in 1,2,3,4 order. This will cause table view to throw exception. More over, cells resulebility will cause also issue. So for your is to sort your data before showing it to cell. And I bet your problem will be resolved.

Create Array of SKShapeNodes. Swift > Xcode 6

In swift, the code below is called at a certain rate (2 times per second) from didMoveToView(). Every time this function is called, a new circlenode should appear on the screen. But instead of continually adding them, when it tries to add the second one it throws an error : attempted to add sknode which already has a parent. I figured out that you can't make a duplicate node on the same view. With that figured out, I came to the conclusion that I would need to make an array of SKShapeNodes so whenever the function is called, it would take one from the array and add it to the view. When a node has reached the bottom (y = -20) in my case, then it would need to remove the node, and make it available again to use.
So, my question: How to I make an array of SKShapeNodes, so when my function below is called it will take a new node and display it to the view? Also, when nodes exit the view, it will need to make the node available again to use.
let circlenode = SKShapeNode(circleOfRadius: 25) //GLOBAL
func thecircle() {
circlenode.strokeColor = UIColor.whiteColor()
circlenode.fillColor = UIColor.redColor()
let initialx = CGFloat(20)
let initialy = CGFloat(1015)
let initialposition = CGPoint(x: initialx, y: initialy)
circlenode.position = initialposition
self.addChild(circlenode)
let action1 = SKAction.moveTo(CGPoint(x: initialx, y: -20), duration: NSTimeInterval(5))
let action2 = SKAction.removeFromParent()
circlenode.runAction(SKAction.sequence([action1, action2]))
}
So you're right in that the problem the code above is that an SKNode can only have one parent.
You have 2 approaches you could take.
Create an array of your SKShapeNodes
Create a new SKShapeNode as needed
The former has the constraint that you need to keep tabs on your total amount of circles, else you'll exceed your bounds. If you remove items, this also means accounting for it. The latter has the overhead of generating the SKShapeNode when you need it. More than likely this will not be an issue.
To create the array you'd do something like (option 1):
var circleArray:[SKShapeNode] = [SKShapeNode]() // Property in your class
// Code in your init or wherever else you want, depending on what class this is.
// 10 is just an arbitrary number
for var i=0;i<10;i++ {
circleArray.append(SKShapeNode(circleOfRadius: 25))
}
When you want to add it in your thecircle, where you were calling self.addChild(circlenode) something like:
if numCircles < circleArray.count {
SKShapeNode *circlenode = circleArray[numCircles]
// Do other initialization here
self.addChild(circlenode)
numCircles++
}
Or you can do (option 2):
SKShapeNode *circlenode = SKShapeNode(circleOfRadius: 25)
// Do other initialization here
self.addChild(circle node)
I'd probably do option 2 in order to not have to deal with any of the management of number of outstanding circles in the array. This is particularly important if you ever remove circles and want to recycle them.