UITableViewAutomaticDimension Cells resize after scroll or pull to refresh - ios

I have added a table view, and I am display image in the cells. I have also added this code:
tableView.estimatedRowHeight = 175
tableView.rowHeight = UITableViewAutomaticDimension
So that the cells resize depending on the image.
When I launch my app though, I get this :
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this(which is correct):
Also if I remove the like button and just has the image alone in a cell, when I launch my app if I wait 3 seconds without touching anything the cells resize on they're own..?!
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
extension TimelineViewController: UITableViewDataSource {
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 46
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return timelineComponent.content.count
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("PostHeader") as! PostHeaderTableViewCell
let post = self.timelineComponent.content[section]
headerCell.post = post
return headerCell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as! PostTableViewCell
//cell.postImageView?.image = UIImage(named: "Background.png")
let post = timelineComponent.content[indexPath.section]
post.downloadImage()
post.fetchLikes()
cell.post = post
cell.layoutIfNeeded()
return cell
}
}
extension TimelineViewController: UITableViewDelegate {
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
timelineComponent.targetWillDisplayEntry(indexPath.section)
}
Download image code:
func downloadImage() {
// 1
image.value = Post.imageCache[self.imageFile!.name]
if image is not downloaded yet, get it
if (image.value == nil) {
imageFile?.getDataInBackgroundWithBlock { (data: NSData?, error: NSError?) -> Void in
if let data = data {
let image = UIImage(data: data, scale: 2.0)!
self.image.value = image
// 2
Post.imageCache[self.imageFile!.name] = image
}
}
}
}
// MARK: PFSubclassing
extension Post: PFSubclassing {
static func parseClassName() -> String {
return "Post"
}
override class func initialize() {
var onceToken : dispatch_once_t = 0;
dispatch_once(&onceToken) {
// inform Parse about this subclass
self.registerSubclass()
// 1
Post.imageCache = NSCacheSwift<String, UIImage>()
}
}
}
And here is my TableViewCell:
var post: Post? {
didSet {
postDisposable?.dispose()
likeDisposable?.dispose()
if let oldValue = oldValue where oldValue != post {
oldValue.image.value = nil
}
if let post = post {
postDisposable = post.image
.bindTo(postImageView.bnd_image)
likeDisposable = post.likes
.observe { (value: [PFUser]?) -> () in
if let value = value {
//self.likesLabel.text = self.stringFromUserList(value)
self.likeButton.selected = value.contains(PFUser.currentUser()!)
// self.likesIconImageView.hidden = (value.count == 0)
} else {
//self.likesLabel.text = ""
self.likeButton.selected = false
//self.likesIconImageView.hidden = true
}
}
}
}
}
Any help is really appreciated!

I guess, you need to reload the cell when the image is finally loaded, because tableView needs to recalculate cell height (and the whole contentHeight) when image with new size arrives
post.downloadImage { _ in
if tableView.indexPathForCell(cell) == indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], animation: .None)
}
}
and downloadImage method needs to call completion closure. Something like that.
func downloadImage(completion: ((UIImage?) -> Void)?) {
if let imageValue = Post.imageCache[self.imageFile!.name] {
image.value = imageValue
completion?(imageValue)
return
}
//if image is not downloaded yet, get it
imageFile?.getDataInBackgroundWithBlock { (data: NSData?, error: NSError?) -> Void in
if let data = data {
let image = UIImage(data: data, scale: 2.0)!
self.image.value = image
// 2
Post.imageCache[self.imageFile!.name] = image
completion?(image)
} else {
completion?(nil)
}
}
}

Related

Limit the amount of cells shown in tableView, load more cells when scroll to last cell

I'm trying to set up a table view that only shows a specific amount of cells. Once that cell has been shown, the user can keep scrolling to show more cells. As of right now I'm retrieving all the JSON data to be shown in viewDidLoad and storing them in an array. Just for example purposes I'm trying to only show 2 cells at first, one the user scrolls to bottom of screen the next cell will appear. This is my code so far:
class DrinkViewController: UIViewController {
#IBOutlet weak var drinkTableView: UITableView!
private let networkManager = NetworkManager.sharedManager
fileprivate var totalDrinksArray: [CocktailModel] = []
fileprivate var drinkImage: UIImage?
fileprivate let DRINK_CELL_REUSE_IDENTIFIER = "drinkCell"
fileprivate let DRINK_SEGUE = "detailDrinkSegue"
var drinksPerPage = 2
var loadingData = false
override func viewDidLoad() {
super.viewDidLoad()
drinkTableView.delegate = self
drinkTableView.dataSource = self
networkManager.getJSONData(function: urlFunction.search, catagory: urlCatagory.cocktail, listCatagory: nil, drinkType: "margarita", isList: false, completion: { data in
self.parseJSONData(data)
})
}
}
extension DrinkViewController {
//MARK: JSON parser
fileprivate func parseJSONData(_ jsonData: Data?){
if let data = jsonData {
do {
let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String : AnyObject]//Parses data into a dictionary
// print(jsonDictionary!)
if let drinkDictionary = jsonDictionary!["drinks"] as? [[String: Any]] {
for drink in drinkDictionary {
let drinkName = drink["strDrink"] as? String ?? ""
let catagory = drink["strCategory"] as? String
let drinkTypeIBA = drink["strIBA"] as? String
let alcoholicType = drink["strAlcoholic"] as? String
let glassType = drink["strGlass"] as? String
let drinkInstructions = drink["strInstructions"] as? String
let drinkThumbnailUrl = drink["strDrinkThumb"] as? String
let cocktailDrink = CocktailModel(drinkName: drinkName, catagory: catagory, drinkTypeIBA: drinkTypeIBA, alcoholicType: alcoholicType, glassType: glassType, drinkInstructions: drinkInstructions, drinkThumbnailUrl: drinkThumbnailUrl)
self.totalDrinksArray.append(cocktailDrink)
}
}
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
DispatchQueue.main.async {
self.drinkTableView.reloadData()
}
}
//MARK: Image Downloader
func updateImage (imageUrl: String, onSucceed: #escaping () -> Void, onFailure: #escaping (_ error:NSError)-> Void){
//named imageData because this is the data to be used to get image, can be named anything
networkManager.downloadImage(imageUrl: imageUrl, onSucceed: { (imageData) in
if let image = UIImage(data: imageData) {
self.drinkImage = image
}
onSucceed()//must call completion handler
}) { (error) in
onFailure(error)
}
}
}
//MARK: Tableview Delegates
extension DrinkViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return drinksPerPage
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = drinkTableView.dequeueReusableCell(withIdentifier: DRINK_CELL_REUSE_IDENTIFIER) as! DrinkCell
//get image from separate url
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{//index out of range error here
updateImage(imageUrl: image, onSucceed: {
if let currentImage = self.drinkImage{
DispatchQueue.main.async {
cell.drinkImage.image = currentImage
}
}
}, onFailure: { (error) in
print(error)
})
}
cell.drinkLabel.text = totalDrinksArray[indexPath.row].drinkName
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let image = totalDrinksArray[indexPath.row].drinkThumbnailUrl{
updateImage(imageUrl: image, onSucceed: {
}, onFailure: { (error) in
print(error)
})
}
performSegue(withIdentifier: DRINK_SEGUE, sender: indexPath.row)
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage
if indexPath.row == lastElement {
self.drinkTableView.reloadData()
}
}
}
I saw this post: tableview-loading-more-cell-when-scroll-to-bottom and implemented the willDisplay function but am getting an "index out of range" error.
Can you tell me why you are doing this if you are getting all results at once then you don't have to limit your display since it is automatically managed by tableview. In tableview all the cells are reused so there will be no memory problem. UITableViewCell will be created when it will be shown.
So no need to limit the cell count.
I dont now what you are doing in your code but:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = drinksPerPage // no need to write this line
if indexPath.row == lastElement { // if block will never be executed since indexPath.row is never equal to drinksPerPage.
// As indexPath starts from zero, So its value will never be 2.
self.drinkTableView.reloadData()
}
}
Your app may be crashing because may be you are getting only one item from server.
If you seriously want to load more then you can try this code:
Declare numberOfItem which should be equal to drinksPerPage
var numberOfItem = drinksPerPage
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return numberOfRows
return numberOfItem
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == numberOfItem - 1 {
if self.totalDrinksArray.count > numberOfItem {
let result = self.totalDrinksArray.count - numberOfItem
if result > drinksPerPage {
numberOfItem = numberOfItem + drinksPerPage
}
else {
numberOfItem = result
}
self.drinkTableView.reloadData()
}
}
}

Single table view through two different NSFetchedResultsControllers with sections

Good morning to everyone. I am using Swift 3.1.1, Xcode 8.3.2. I need to connect a single Table View to two different tables (entities) in Core Data through two different NSFetchedResultsControllers. I have created two NSFetchedResultsControllers, and even fetched data from table but I faced problem how to tell Table View that first controller should response for section one and second controller should be responsible for section two.
I can show you the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tv: UITableView!
#IBAction func pressed(_ sender: UIButton) {
ModelA.read { table1 in
ModelB.read { table2 in
if table1.isEmpty {
ModelA.save(recordToSave: [(a: 1, b: "a"), (a: 2, b: "b"), (a: 3, b: "c")]) {
ModelB.save(recordToSave: [(a: 4, b: 5.0, c: true), (a: 6, b: 7.0, c: false)]) {
self.tvReload()
}
}
} else {
self.tvReload()
}
}
}
}
var fetchedResultsControllerForModelA = CoreDataFunctions.fetchedResultsController
var fetchedResultsControllerForModelB = CoreDataFunctions.fetchedResultsController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tvReload()
}
func modelOfTableA(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
if let fetchedResultsControllerForModelA = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelA, indexPath: indexPath) {
if let model = ModelA.read(nsmanagedobject: fetchedResultsControllerForModelA) {
return (forLabel1: "\(model.a)", forLabel2: model.b, forLabel3: "")
}
}
return nil
}
func modelOfTableB(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
if let fetchedResultsControllerForModelB = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelB, indexPath: indexPath) {
if let model = ModelB.read(nsmanagedobject: fetchedResultsControllerForModelB) {
return (forLabel1: "\(model.a)", forLabel2: "\(model.b)", forLabel3: "\(model.c)")
}
}
return nil
}
func tvReload() {
fetchedResultsControllerForModelA = CoreDataFunctions(tableName: .a).fetchedResultsController(keyForSort: ModelA.a.rawValue, searchParameters: nil)
fetchedResultsControllerForModelB = CoreDataFunctions(tableName: .b).fetchedResultsController(keyForSort: ModelB.a.rawValue, searchParameters: nil)
do {
try fetchedResultsControllerForModelA?.performFetch()
try fetchedResultsControllerForModelB?.performFetch()
DispatchQueue.main.async {
self.tv.reloadData()
}
} catch {
print("Error")
}
}
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections1 = fetchedResultsControllerForModelA?.sections {
if let sections2 = fetchedResultsControllerForModelB?.sections {
return sections1[section].numberOfObjects + sections2[section].numberOfObjects
}
return sections1[section].numberOfObjects
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
if indexPath.section == 0 {
if let modelOfTable = modelOfTableA(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
} else {
if let modelOfTable = modelOfTableB(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
}
return cell
}
}
I could not find any tutorial on this theme, so I am asking question there. I do not want to use inheritance from single entity in Core Data, because, in real life it would be impossible.
Thank you for any help or advice!
OK - I downloaded your code, and there are a couple issues...
1) If you want your data to fill two sections in the table, the table must have two sections - currently, you are just returning 1, so use this (although you may want/need different handling based on data retrieved):
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
if fetchedResultsControllerForModelA == nil || fetchedResultsControllerForModelB == nil {
return 0
}
return 2
}
2) For number of rows in each section, your code was close but not quite right...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if let sections = fetchedResultsControllerForModelA?.sections {
return sections[0].numberOfObjects
}
} else {
if let sections = fetchedResultsControllerForModelB?.sections {
return sections[0].numberOfObjects
}
}
return 0
}
3) For the actual cellForRowAtIndexPath data, again your code was close but you need to keep track of which data to get for each section...
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
if indexPath.section == 0 {
if let modelOfTable = modelOfTableA(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
} else {
// Each "Table data model" has only one section... since this is the 2nd table section, we need
// to change the section of the index path for our call to the data model
let dataIndexPath = IndexPath(row: indexPath.row, section: 0)
if let modelOfTable = modelOfTableB(indexPath: dataIndexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
}
return cell
}
That should get you on your way.

Using Almofireimage to set image in a Custom Tableview Cell

I'm using Alamofireimage to set an image based on a remote url on a UIImageView in my Custom UITableViewCell however the results are (1) the images aren't set until you scroll and (2) even though I'm using StackViews for my autolayout the sizes of the images displayed in the app vary wildly. Any idea what I'm doing wrong?
import UIKit
import Alamofire
import AlamofireImage
class AppsTableViewController: UITableViewController {
let session = URLSession.shared
var rekos: [Reko]? {
didSet {
DispatchQueue.main.async {
self.tableView?.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
RekoManager().downloadAndConvertRekos(rekoType: .apps) { (result) in
print("Reko Count = \(result.count)")
self.rekos = result
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let count = rekos?.count else {return 0}
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppCell", for: indexPath) as! AppsTableViewCell
if let reko = rekos?[indexPath.row] {
cell.titleLabel.text = reko.title
cell.descriptionLabel.text = reko.description
if let imageUrlString = reko.imageUrlString {
if let imageURL = URL(string: imageUrlString) {
//TODO IMPLEMENT SPINNER OVER IMAGE
cell.appImageView.af_setImage(withURL: imageURL, placeholderImage: UIImage(named: "placeholder"), filter: nil, progress: nil, progressQueue: DispatchQueue.main, imageTransition: .noTransition, runImageTransitionIfCached: false, completion: { (result) in
cell.setNeedsLayout()
cell.layoutIfNeeded()
})
}
}
}
return cell
}
}

Creating TableViews in Swift with an Array

I'm attempting to use the result of one Rest call as an input for my TableView.
I've got an array named GamesList[String] that is synthesized in the viewDidLoad() function. This is the viewDidLoad() fuction:
override func viewDidLoad() {
super.viewDidLoad()
getState() { (json, error) -> Void in
if let er = error {
println("\(er)")
} else {
var json = JSON(json!);
print(json);
let count: Int = json["games"].array!.count
println("found \(count) challenges")
for index in 0...count-1{
println(index);
self.GamesList.append(json["games"][index]["game_guid"].string!);
}
}
}
}
The problem is that the functions for filling the TableView get executed before my GamesList array is filled up. These are the functions that fill the TableView:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return GamesList.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Game", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel?.text = GamesList[indexPath.row]
cell.detailTextLabel?.text = GamesList[indexPath.row]
return cell
}
How do I force the tables to get filled up (refreshed) after my array has been filled?
use self.tableView.reloadData() after you append your values
getState() { (json, error) -> Void in
if let er = error {
println("\(er)")
} else {
var json = JSON(json!);
print(json);
let count: Int = json["games"].array!.count
println("found \(count) challenges")
for index in 0...count-1{
println(index);
self.GamesList.append(json["games"][index]["game_guid"].string!);
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}

Swift: Display image from UIImage array in table view

I am getting images from an async request and adding them to a [UIImage]() so that I can populate my UITableView images with those from the array. The problem is, I keep getting Fatal error: Array index out of range in the cellForRowAtIndexPath function when this is called, and I suspect it may be because I'm making an async call? Why can't I add an image from the array to the table view row?
var recommendedImages = [UIImage]()
var jsonLoaded:Bool = false {
didSet {
if jsonLoaded {
// Reload tableView on main thread
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
dispatch_async(dispatch_get_main_queue()) { // 2
self.tableView.reloadData() // 3
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let imageURL = NSURL(string: "\(thumbnail)")
let imageURLRequest = NSURLRequest(URL: imageURL!)
NSURLConnection.sendAsynchronousRequest(imageURLRequest, queue: NSOperationQueue.mainQueue(), completionHandler: { response, data, error in
if error != nil {
println("There was an error")
} else {
let image = UIImage(data: data)
self.recommendedImages.append(image!)
self.jsonLoaded = true
}
})
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var songCell = tableView.dequeueReusableCellWithIdentifier("songCell", forIndexPath: indexPath) as! RecommendationCell
songCell.recommendationThumbnail.image = recommendedImages[indexPath.row]
return songCell
}
Edit: My numberOfRowsInSection method. recommendedTitles is from the same block of code that I excluded. It's always going to be 6.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recommendedTitles.count
}
Your error is you return 6 in numberOfRowsInSection,so tableview know that you have 6 cell
But,when execute cellForRowAtIndexPath,your image array is empty,so it crashed.
Try this
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recommendedImages.count
}
Also switch to main queue,this is enough
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})

Resources