Array index out of range - Downloading images from Parse - ios

I am trying to query images from parse, and when I open my app everything works correctly, but if I try and refresh I get this crash shown below...
Not too sure what is causing this...To explain a bit about how I have things set up :
I have a tableView with 1 cell, in that cell are three imageView connected to a collection Outlet. Then I am getting my images from parse and placing them in my imageViews, then in the numberOfRowsInSection I divide it by 3 so it doesn't repeat the image 3 times...!
Here's my code below:
var countries = [PFObject]()
override func viewDidLoad() {
super.viewDidLoad()
loadPosts()
// Do any additional setup after loading the view.
}
func loadPosts() {
PFUser.query()
// Build a parse query object
let query = PFQuery(className:"Post")
query.whereKey("user", equalTo: PFUser.currentUser()!)
query.orderByDescending("createdAt")
// Fetch data from the parse platform
query.findObjectsInBackgroundWithBlock {
(objects: [PFObject]?, error: NSError?) -> Void in
// The find succeeded now rocess the found objects into the countries array
if error == nil {
self.countries.removeAll(keepCapacity: true)
if let objects = objects {
self.countries = Array(objects.generate())
}
// reload our data into the collection view
self.tableView.reloadData()
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return countries.count / 3
}
var counter = 0
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("cell2") as! bdbTableViewCell
for imageView in cell.threeImages {
let placeHolder = UIImage(named: "camera")
imageView.image = placeHolder
let finalImage = countries[counter++]["imageFile"]
finalImage!.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
imageView.image = UIImage(data:imageData)
}
}
}}
return cell
}

If I understand your data, need to move to the spot in the array that has your country's first image, then iterate through the next 3 images.
Basically, you should move the counter declaration inside the function body and initialize it with the location of the first image. See the method definition below for the full implementation.
Your code works the first time by shear luck. Basically iOS will request the cell at row 1, and your counter will increment itself by getting images at index 0,1,and 2. Then iOS will request the cell at row 2, and it will pull images 3,4,5. The counter will keep incrementing. Then, when you refresh the table, the counter is still set to a number higher than the size of your images array, so you get a crash. You would also display image incorrectly if iOS for whatever reason asked for the cells in a different order. You need to use the indexPath.row parameter to get the right images.
Your code also does not account for additional images after the end of a multiple of 3. So if there are 17 images ,images 16 1nd 17 will not be given a row in the tableview. To correct that, add the following function:
func roundUp(value: Double) -> Int {
if value == Double(Int(value)) {
return Int(value)
} else if value < 0 {
return Int(value)
} else {
return Int(value) + 1
}
}
Then change the tableView's numberOfRowsInSection method to the following:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return roundUp(Double(countries.count / 3.0))
}
Finally, in your cellForRowAtIndexPath function, you will have to handle potentially trying to look for an image past the size of your array:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var imageIndex = (indexPath.row * 3)
let cell = tableView.dequeueReusableCellWithIdentifier("cell2") as! bdbTableViewCell
for imageView in cell.threeImages {
let placeHolder = UIImage(named: "camera")
imageView.image = placeHolder
if imageIndex < countries.count
{
let finalImage = countries[imageIndex++]["imageFile"]
finalImage!.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
imageView.image = UIImage(data:imageData)
}
}
}
}
return cell
}
I believe this will fix all of your problems. I do suggest you take a tutorial on how UITableView works, as well as how to step through your own code to troubleshoot. That way you can understand why your original attempt wouldn't work.

var counter = 0
should be reset before function
cellForRowAtIndexPath

Problem
You declared counter variable as static. That's why it is not being reset between cellForRowAtIndexPath calls but it gets incremented each time the method is invoked.
Solution:
Move counter declaration to the function body. Like below:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var counter = 0
let cell = tableView.dequeueReusableCellWithIdentifier("cell2") as! bdbTableViewCell
for imageView in cell.threeImages {
let placeHolder = UIImage(named: "camera")
imageView.image = placeHolder
let finalImage = countries[counter++]["imageFile"]
finalImage!.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
imageView.image = UIImage(data:imageData)
}
}
}}
return cell
}

Related

iOS - Set numberOfSections in tableView to number of filtered cells (Swift)

I've set up my tableView so it filters based on the value of 2 factors of every child of the array (factor1 and factor2) matching with 2 user inputs (factor1UserInput and factor2UserInput). The problem is- because I have my numberOfSections set to return array.count- it loads ALL the cells, including unpopulated cells, for every child in the array.
So, I'm attempting to set my numberOfSections in my tableView to the number of filtered cells (the cells I'm populating) - unless there is a better way to filter these in order to not load unpopulated cells.
numberOfSections code:
func numberOfSections(in tableView: UITableView) -> Int {
return array.count
}
tableView code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExampleCell", for: indexPath)
let preview = array[indexPath.section]
// BELOW LINE FILTERS THE DATA
if preview.factor1 == factor1UserInput && preview.factor2 == factor2UserInput {
// TRYING TO ONLY LOAD THESE CELLS
print("something matches the filter")
cell.imageView?.image = UIImage(named: "PlaceholderImage")
if let imageUrl = preview.imageUrl {
let url = URL(string: imageUrl)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error!)
return
}
DispatchQueue.main.async {
cell.imageView?.image = UIImage(data: data!)
}
}.resume()
}
} else {
}
return cell
}
In your case, you should not use the self.array as the source for the tableView, instead, you should declare a proxy variable and use it as the source, something like:
var displayArray: [YourClass] {
return array.filter({ (YourClass) -> Bool in
return /*Your condition here*/
})
}
And then in your table view datasource:
func numberOfSections(in tableView: UITableView) -> Int {
return displayArray.count
}
And then apply the same idea for cellForRowAtIndexPath

UITableViewCells won't load index is out of range

I am trying to load some data from CloudKit to populate a tableview with custom cells and I am having difficulty getting the data to appear.
When I define the number of rows as the count of the CKRecord array the tableview shows up, but with nothing loaded into them. They are just spaced out correctly for the images to be in there. Also, when I set breakpoints at let record = matches[indexPath.row] it won't trigger.
However, if I change the return matches.count to an actual number, the project crashes at let record = matches[indexPath.row]
with the error that the index is out of range. I want to keep the number of rows as the count for the record array, but that is the only change that will actually execute the override function that calls the tableview cell.
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matches.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Card", for: indexPath) as! MatchTableViewCell
let record = matches[indexPath.row]
if let img = record.value(forKey: "Picture") as? CKAsset {
cell.profileimg.image = UIImage(contentsOfFile: img.fileURL.path)
}
return cell
}
Any advice is appreciated
Update- here is where I load the model
func loadModel() {
let totalMatch = (defaults.object(forKey: "passedmatch") as! [String] )
let predicateMatch = NSPredicate(format: "not (UserID IN %#)", totalMatch )
ProfilesbyLocation = defaults.object(forKey: "Location") as! String
let query = CKQuery(recordType: ProfilesbyLocation , predicate: predicateMatch )
publicData.perform(query, inZoneWith: nil,completionHandler: ({results, error in
print("loading")
if (error != nil) {
let nsError = error! as NSError
print(nsError.localizedDescription)
print ("error")
} else {
if results!.count > 0 {
DispatchQueue.main.async() {
self.tableView.reloadData()
self.refresh.endRefreshing()
print("refreshed")
}
}
}
}
)
)}
It sounds like you are not calling
tableview.reloadData()
after your asynchronous data comes in. Leave numberOfRowsInSection as it is and make sure when your network request comes back you are reloading. That should do it.
If you received your data from background thread(or queue), please make sure when you update your UI elements under the main thread(or queue). Try this:
DispatchQueue.main.async {
tableView.reloadData()
}
This solved my problem.
First, disconnect the Delegate/DataSource of the TableView from the Interface Builder refer this image. When you have finally prepared your data that is matches array then add this.
`DispatchQueue.main.async {
tableView.dataSource = self
tableView.delegate = self
tableView.reloadData
}`
Hope this helps.
It looks like your matches is incorrect. Before you call func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
, you should check your matches.
If you are still not sure about that. You can init your matches with string literals and have a try.

Swift tableView Pagination

I have success working tableview with json parsing code. But may have 1000 more item so I need pagination when scrolling bottom side. I don't know how can I do this for my code shown below. For objective-C, there are a lot of examples but for Swift I didn't find a working example.
import UIKit
class ViewController: UIViewController, UITableViewDataSource,UITableViewDelegate {
let kSuccessTitle = "Congratulations"
let kErrorTitle = "Connection error"
let kNoticeTitle = "Notice"
let kWarningTitle = "Warning"
let kInfoTitle = "Info"
let kSubtitle = "You've just displayed this awesome Pop Up View"
#IBOutlet weak var myTableView: UITableView!
#IBOutlet weak var myActivityIndicator: UIActivityIndicatorView!
var privateList = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
loadItems()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return privateList.count
}
internal func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell:myCell = tableView.dequeueReusableCellWithIdentifier("myCell") as! myCell
cell.titleLabel.text = privateList[indexPath.row]
return cell
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == UITableViewCellEditingStyle.Delete){
print(indexPath.row)
let alert = SCLAlertView()
alert.addButton("Hayır"){ }
alert.addButton("Evet") {
self.myTableView.beginUpdates()
self.privateList.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Left)
print("Silindi")
self.myTableView.endUpdates()
self.loadItems()
}
alert.showSuccess(kSuccessTitle, subTitle: kSubtitle)
}
}
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// the cells you would like the actions to appear needs to be editable
return true
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "Detail") {
let destinationView = segue.destinationViewController as! DetailViewController
if let indexPath = myTableView.indexPathForCell(sender as! UITableViewCell) {
destinationView.privateLista = privateList[indexPath.row]
}
}
}
internal func tableView(tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
{
return 0.0
}
func loadItems()
{
loadItemsNow("privateList")
}
func loadItemsNow(listType:String){
myActivityIndicator.startAnimating()
let listUrlString = "http://bla.com/json2.php?listType=" + listType + "&t=" + NSUUID().UUIDString
let myUrl = NSURL(string: listUrlString);
let request = NSMutableURLRequest(URL:myUrl!);
request.HTTPMethod = "GET";
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
print(error!.localizedDescription)
dispatch_async(dispatch_get_main_queue(),{
self.myActivityIndicator.stopAnimating()
})
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? NSArray
if let parseJSON = json {
self.privateList = parseJSON as! [String]
}
} catch {
print(error)
}
dispatch_async(dispatch_get_main_queue(),{
self.myActivityIndicator.stopAnimating()
self.myTableView.reloadData()
})
}
task.resume()
}
}
For that you need to have server side change also.
Server will accept fromIndex and batchSize in the API url as query param.
let listUrlString = "http://bla.com/json2.php?listType=" + listType + "&t=" + NSUUID().UUIDString + "&batchSize=" + batchSize + "&fromIndex=" + fromIndex
In the server response, there will be an extra key totalItems. This will be used to identify all items are received or not. An array or items fromIndex to batchSize number of items.
In the app side
First loadItem() will be called with fromIndex = 0 and batchSize = 20 (for example in viewDidLoad() or viewWillAppear). removeAll items from privateList array before calling loadItem() for the first time
Server returns an array of first 20 items and totalItems total number of items in the server.
Append the 20 items in privateList array and reload tableView
In tableView:cellForRowAtIndexPath method check if the cell is the last cell. And check if totalItems (form server) is greater than privateList.count. That means there are more items in the server to load
if indexPath.row == privateList.count - 1 { // last cell
if totalItems > privateList.count { // more items to fetch
loadItem() // increment `fromIndex` by 20 before server call
}
}
Question: where is refresh ? will be scrolling ?
Refresh after appending new items in the array when server response received. (step 3)
Scrolling will trigger tableView:cellForRowAtIndexPath for every cell when user scrolls. Code is checking if it is the last cell and fetch remaining items. (step 4)
Sample project added: https://github.com/rishi420/TableViewPaging
SWIFT 3.0 and 4.0
If you're sending the page number in the API request then this is the ideal way for implementing pagination in your app.
declare the variable current Page with initial Value 0 and a bool to check if any list is being loaded with initial value false
var currentPage : Int = 0
var isLoadingList : Bool = false
This is the function that gets the list example:
func getListFromServer(_ pageNumber: Int){
self.isLoadingList = false
self.table.reloadData()
}
This is the function that increments page number and calls the API function
func loadMoreItemsForList(){
currentPage += 1
getListFromServer(currentPage)
}
this is the method that will be called when the scrollView scrolls
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (((scrollView.contentOffset.y + scrollView.frame.size.height) > scrollView.contentSize.height ) && !isLoadingList){
self.isLoadingList = true
self.loadMoreItemsForList()
}
}
P.S. the bool isLoadingList role is to prevent the scroll view from getting more lists in one drag to the bottom of the table view.
The good and efficient way to do it is by using scrollviewDelegate in tableview
Just add UIScrollViewDelegate in your viewController
In view controller
//For Pagination
var isDataLoading:Bool=false
var pageNo:Int=0
var limit:Int=20
var offset:Int=0 //pageNo*limit
var didEndReached:Bool=false
viewDidLoad(_){
tableview.delegate=self //To enable scrollviewdelegate
}
Override two methods from this delegate
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
print("scrollViewWillBeginDragging")
isDataLoading = false
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
print("scrollViewDidEndDecelerating")
}
//Pagination
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
print("scrollViewDidEndDragging")
if ((tableView.contentOffset.y + tableView.frame.size.height) >= tableView.contentSize.height)
{
if !isDataLoading{
isDataLoading = true
self.pageNo=self.pageNo+1
self.limit=self.limit+10
self.offset=self.limit * self.pageNo
loadCallLogData(offset: self.offset, limit: self.limit)
}
}
}
This is now a little bit easier with the addition of a new protocol in iOS10: UITableViewDataSourcePrefetching
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
//It works fine
func getPageCount(TotalCount : Int) -> Int{
var num = TotalCount
let reminder = num % 50
print(reminder)
if reminder != 0{
num = TotalCount/50
num = num + 1
}else{
num = TotalCount/50
}
return num
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let TotalPage = self.getPageCount(TotalCount: Int(Datacount)!)
let lastItem = self.mainArr.count - 1
if indexPath.row == lastItem {
print("IndexRow\(indexPath.row)")
if self.page < TotalPage-1 {
self.view_Loader.isHidden = false
self.view_LoaderHeight.constant = 50
self.page += 1
self.YourAPI()
}
}
}`
By using UITableViewDelegate, u can call the function
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastItem = self.mes.count - 1
if indexPath.row == lastItem {
print("IndexRow\(indexPath.row)")
if currentPage < totalPage {
currentPage += 1
//Get data from Server
}
}
}
I needed something similar on a project and my solution was:
1 - create a variable numberOfObjectsInSubArray (initial value 30 or whatever you want)
2 - create a subarray to add a number of objects from your privateList array every time i tap "show more"
let subArray = privateList?.subarrayWithRange(NSMakeRange(0, numberOfObjectsInSubArray))
And use it on
internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return subArray.count
}
3- Whenever you need to show more objects, do:
func addMoreObjectsOnTableView () {
numberOfObjectsInSubArray += 30
if (numberOfObjectsInSubArray < privateList.count) {
subArray = privateList?.subarrayWithRange(NSMakeRange(0, numberOfObjectsInSubArray))
} else {
subArray = privateList?.subarrayWithRange(NSMakeRange(0, privateList.count))
}
tableView.reloadData()
}
I hope it helps
I've tried an approach with willDisplayCell. But it produces unwanted stops during scrolling which makes the user experience not good.
I think a better way is to do it in scrollViewDidEndDecelerating delegate method. It calls when the scroll finishes and only then new data comes. User sees that there is new content and scroll again if he wants. I've taken the answer here but instead of scrollViewDidEndDragging I use scrollViewDidEndDecelerating. It looks just better in my case. Here is some code from my project.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard scrollView == tableView,
(scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height,
!viewModel.isLastPeriodicsPage else { return }
viewModel.paginatePeriodics(tableView.getLastIndexPath())
}
Another way of doing this is: You may set a threshold for getting elements while sending request each time:
Lets say you you are fetching 20 elements first time. You will be saving last fetched record id or number for getting list of next 20 elements.
let lastFetchedIndex = 20;
I am assuming that you have already added these records in your myArray. MyArray is the dataSource of tableView. Now myArray is containing 40 objects. I am going to make a list of indexPaths of rows that needs to be inserted in tableView now.
var indexPathsArray = [NSIndexPath]()
for index in lastFetchedIndex..<myArray.count{
let indexPath = NSIndexPath(forRow: index, inSection: 0)
indexPathsArray.append(indexPath)
}
Here I am updating my tableView. Make sure your dataSource i mean your myArray has already been updated. So that it may insert rows properly.
self.tableView.beginUpdates()
tableView!.insertRowsAtIndexPaths(indexPathsArray, withRowAnimation: .Fade)
self.tableView.endUpdates()
Add another section to your tableview, let this section have only 1 row which will be a cell containing an activity indicator, to denote loading.
internal func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 2;
}
internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if section == 0 {
return privateList.count
} else if section == 1 { // this is going to be the last section with just 1 cell which will show the loading indicator
return 1
}
}
internal func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
if section == 0 {
let cell:myCell = tableView.dequeueReusableCellWithIdentifier("myCell") as! myCell
cell.titleLabel.text = privateList[indexPath.row]
return cell
} else if section == 1 {
//create the cell to show loading indicator
...
//here we call loadItems so that there is an indication that something is loading and once loaded we relaod the tableview
self.loadItems()
}
}
here is a sample code for collection view :
var page = 0
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell{
print("page Num:\(page)")
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath){
if arrImagesData.count-1 == indexPath.row && arrImagesData.count%10 == 0{
getMoreImages(page)
}
}
func getMoreImages(page:Int){
//hit api
if api_success == true {
if self.page == 0 {
self.arrImagesData.removeAll()
}
self.arrImagesData.appendContentsOf(api_data)
self.collectionImages.reloadData()
self.page = self.page + 1
}
}
API handler is api handler for network call that just do POST and GET calls. getNotifications is basically just a post call with params( offset and pageSize ) and in response there is list.
Main logic is changing offset depending on cell in willDisplay collectionView delegate. Comment if you having any question , happy to help.
var isFetching: Bool = false
var offset = 0
var totalListOnServerCount = 20 // it must be returned from server
var pageSize = 10 // get 10 objects for instance
// MARK: - API Handler
private func fetchNotifications(){
// return from function if already fetching list
guard !isFetching else {return}
if offset == 0{
// empty list for first call i.e offset = 0
self.anyList.removeAll()
self.collectionView.reloadData()
}
isFetching = true
// API call to fetch notifications with given offset or page number depends on server logic just simple POST Call
APIHandler.shared.getNotifications(offset: offset) {[weak self] (response, error) in
if let response = response {
self?.isFetching = false
if self?.offset == 0{
// fetch response from server for first fetch
self?.notificationsResponse = response
if self?.refreshControl.isRefreshing ?? false {
self?.refreshControl.endRefreshing()
}
}else{
// append if already exist ( pagination )
self?.notificationsResponse?.notifications.append(contentsOf: response.notifications)
}
self?.collectionView.reloadData()
}
}
}
// MARK: - Collection View Delegate
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let anyList = responseFromServer else { return }
// check if scroll reach last index available and keep fetching till our model list has all entries from server
if indexPath.item == anyList.count - 1 && anyList.count < totalListOnServerCount{
offset += pageSize
fetchNotifications()
}
}
Made a General purpouse pagination framework: 🎉
https://github.com/eonist/PaginationTable
let table = Table(rowData: [], frame: .zero, style: .plain)
view = table
table.isFetching = true
Table.fetchData(range: table.paginationRange) { rowItem in
DispatchQueue.main.async { [weak table] in
table?.rowData += rowItem
table?.reloadData()
table?.paginationIndex += Table.paginationAmount // set the new pagination index
table?.isFetching = false
}
}
Swift 5 (Full comprehensive pagination solution)
The UI code:
https://github.com/eonist/PaginationTable
The Data Model code:
https://github.com/eonist/PaginationService
Core components:
rowData: This array will grow on each scroll-ended-event until it has loaded all items from backend-API
paginationAmount: The amount to fetch on each pagination cycle
paginationIndex: The current amount of cells (this grows as you load more data
isFetching: A boolean that lets the code know if data is already loading or not, to avoid double fetching etc
fetchData: Simulates getting data from remote-api
Gotchas:
The example code is not reliant on a backend. It simply tests with data from a file and simulates network calls by sleeping for some seconds
The example uses some dependencies in order to speed up the creation of this example. But its basic stuff like AFNetwork, Json parsing, Autollayout. All of which could easily be substituted
Requirements:
Backend-API that can provide the count of items
Backend-API that can return items for a range (startIndex, endIndex)

UITableViewCell backgroundView image is not downloading & showing right images

I have a customized UITableViewCell with some attributes. The goal is to update each cell with a different background image (downloaded from the web asynchronously). I have ten cells to show, but the problem is that the bottom 5 cells always show the first five images downloaded (thats downloaded for the top 5 cells).
To avoid downloading the images again when a cell reloads when user scrolls, I use a table view cell attribute called isImageLoaded. See the snippet below. Tried multiple solutions to fix this without avail. I do show text attributes for these cells as well (not fetched through HTTP requests) and they show correct values for all the 10 cells scrolling up and down.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:ImageTableCell = self.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! ImageTableCell
cell.tag = indexPath.section
// cell.imageLoaded is initialized as false in the custom cell settings
if !(cell.imageLoaded) {
var photourl:String = "http://someurl/image" + String(indexPath.section)
downloadImage(cell, url: NSURL(string: photourl)!)
}
}
func downloadImage(cell:HomeSummaryTableCell, url:NSURL){
cell.activityindicator.startAnimating()
println("Started downloading \"\(url.lastPathComponent!.stringByDeletingPathExtension)\".")
getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
println("Finished downloading \"\(url.lastPathComponent!.stringByDeletingPathExtension)\".")
var imageView:UIImageView = UIImageView(image: UIImage(data: data!))
imageView.contentMode = UIViewContentMode.ScaleAspectFill
imageView.clipsToBounds = true
cell.backgroundView = imageView
cell.activityindicator.stopAnimating()
cell.imageLoaded = true
}
}
}
func getDataFromUrl(urL:NSURL, completion: ((data: NSData?) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(urL) { (data, response, error) in
completion(data: data)
}.resume()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 10
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
This is happening because dequed cell retains the values of set properties, so you need to reset your imageLoaded parameter when preparing for reuse.
In your custom cell, override prepareForReuse and set imageLoaded to false.
override func prepareForReuse() {
super.prepareForReuse()
imageLoaded = false
}

UIImageViews are repeating in cell

I have a cell that holds an UIScrollView which will have a few UIImageViews which will hold images downloaded from Parse.
This works great when it loads the first 5 in my tableview, but after the fifth, more and more UIImageViews are stacked over one another.
I believe that problem lies when I use the:
cell.scrollView.addSubview(myImageView)
This adds and keeps adding new UIImageViews in the scrollView.
I've been trying different things to remove the UIImageViews but I can't seem to implement it correctly.
I'm back to square one but now I know what is causing the problem.
This is how I'm downloading my images from Parse:
photoQuery.findObjectsInBackgroundWithBlock {
(objects:[AnyObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects!{
var arrayFile = [PFFile]() // hold both image into temp array
if object.objectForKey("imageOne") != nil {
self.resultsHasImageOneFile.append(object.objectForKey("imageOne") as! PFFile)
arrayFile.append(object.objectForKey("imageOne") as! PFFile)
}
if object.objectForKey("imageTwo") != nil {
self.resultsHasImageTwoFile.append(object.objectForKey("imageTwo") as! PFFile)
arrayFile.append(object.objectForKey("imageTwo") as! PFFile)
}
// will add imageThree
// will add imageFour
// will add imageFive
self.masterImageArray.append(arrayFile)//add temp array into Master Image array
self.resultsTable.reloadData()
}
}
}
this is how i'm adding my UIImageViews into my scrollView:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// configure cell....
var imageArray = masterArray[indexPath.row]
cell.imageScrollView.tag = indexPath.row // do something with this?????
for var i = 0; i < imageArray.count; i++ {
var myImageView: UIImageView = UIImageView()
myImageView.frame.size.width = imageWidth
myImageView.frame.size.height = imageHeight
myImageView.frame.origin.x = xPosition
imageArray[i].getDataInBackgroundWithBlock{
(imageData: NSData?, error: NSError?) -> Void in
myImageView.image = UIImage(data: imageData!)
}
cell.imageScrollView.addSubview(myImageView)
xPosition = imageWidth
scrollViewContentSize += imageWidth
cell.imageScrollView.contentSize = CGSize(width: scrollViewContentSize, height: imageHeight)
cell.imageScrollView.tag = indexPath.row // do something with this?????
}
}
I've tried to remove what's inside the cell content with the following but it says it has found nil, which crashes.
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
var cell = FriendsFeedTableViewCell()
cell.imageScrollView.removeFromSuperview()
}
in my FriendsFeedTableViewCell class, I only have a label, and a UIScrollView which is named
#IBOutlet weak var imageScrollView: UIScrollView!
How can I properly remove the already created UIImageViews?
I believed that you are doing too much work by accessing Parse Twice, you should download every single images once and insert them into a single array of objects. Example :
var photoQuery = PFQuery(className: "")
photoQuery.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objects = objects as? [PFObject]
{
for singleObject in objects
{
var ImageDataToDownload = singleObject["images"] as! PFFile
ImageDataToDownload.getDataInBackgroundWithBlock({ (dataToget:NSData?, error:NSError?) -> Void in
if error == nil
{
if let image = UIImage(data: dataToget!) {
// pass this image to your array
// reload tableview
}
}
})
}}}}
then in cellForRowAtIndexPath()
cell.imageView.image = arrayFromQuery[indexPath.row]
then you take care of your scrollview inside that cell

Resources