I am trying to load my tableview without using dequeueReusableCell, but it just crashes my app, cant figure out what i am doing wrong?
let cell = Tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SökrutaCell // THIS CODE WORKS FINE
let cell = SökrutaCell() // THIS CODE CRASHES - Unexpectedly found nil while unwrapping an optional
Would love if anyone could point me in the right direction so i can understand what is failing.
import UIKit
import Alamofire
class Sökruta: UIViewController {
var sökresultat = [Object2]()
#IBOutlet weak var Tableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
GetRequest(Url: "https://randomurl.se")
Tableview.delegate = self
Tableview.dataSource = self
Tableview.backgroundColor = UIColor.white
// Do any additional setup after loading the view.
}
}
// MARK: - Delegate extension 1
extension Sökruta: UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("you tapped me!")
}
}
// MARK: - Delegate extension 2
extension Sökruta: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sökresultat.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let cell = Tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SökrutaCell // THIS CODE WORKS FINE
let cell = SökrutaCell() // THIS CODE CRASHES - Unexpectedly found nil while unwrapping an optional
cell.Söknamn.text = sökresultat[indexPath.row].displayname
if let url = URL(string: dontmindthiscode) {
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: url)
DispatchQueue.main.async {
cell.Sökbild.image = UIImage(data: data)
}
} catch let err {
print("error: \(err.localizedDescription)")
}
}
}
else
{
DispatchQueue.main.async {
cell.Sökbild.image = UIImage(systemName: "eye.slash")
}
}
return cell
}
}
// MARK: - Extension functions
extension Sökruta{
// MARK: - GET REQUEST
func GetRequest(Url: String) {
// MARK: - Login details
let headers: HTTPHeaders = [
.authorization(username: "username", password: "password"),
.accept("application/json")]
// MARK: - Api request
AF.request(result, headers: headers).validate().responseJSON { response in
// MARK: - Check for errors
if let error = response.error
{
print (error)
return}
// MARK: - Print response
if response.response != nil
{ }
// MARK: - Print data
if response.data != nil
{
let decoder = JSONDecoder()
do
{
let api = try decoder.decode(Main2.self, from: response.data!)
self.sökresultat = api.objects
self.Tableview.reloadData()
}
catch {
print(error.localizedDescription)
print("Error in JSON parsing")
}
}
}
}// MARK: - END
}
This is my cell.swift code:
import UIKit
class SökrutaCell: UITableViewCell {
#IBOutlet weak var Sökbild: UIImageView!
#IBOutlet weak var Söknamn: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
You have a class with #IBOutlet references to a prototype cell in your storyboard. You can't use that cell prototype (with its custom layout) and have the outlets hooked up for you unless you use the dequeueReusableCell(withIdentifier:for:).
If your goal was to compare this against a non-reused cell, you could instantiate a UITableViewCell programmatically and use the built in textLabel and imageView properties. For example, you could:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "...")
cell.textLabel?.text = ...
cell.imageView?.image = ... // use a placeholder image here or else it won't show the image view at all!
someMethodToFetchImageAsynchronously { image in
// make sure this cell hasn't scrolled out of view!!!
guard let cell = tableView.cellForRow(at: indexPath) else { return }
// if still visible, update its image
cell.imageView?.image = image
}
return cell
}
Or you could consider more complicated programmatic patterns (where you add controls and configure them manually, which probably would be a more fair comparison), too. But the above is a fairly minimal implementation.
Needless to say, this pattern is not recommended, as you lose the storyboard cell prototype benefits and the performance/memory benefits of cell reuse. But if your goal was just to compare and contrast the memory, performance, and software design considerations, perhaps this helps you get your arms around it.
While I've attempted to answer your question, before I worried about the inherent performance of dequeued cells, your image retrieval mechanism is a far greater performance concern. You are fetching images using Data(contentsOf:), a non-cancelable network request. So, if you have hundred rows, and you quickly scroll down to rows 90..<100, the image retrieval for those 10 rows will be backlogged being the network requests for the first 90 rows! Also, are your images appropriately sized for the small image view in the cell? That can also observably impact performance and the smoothness of the scrolling.
There are a number of decent asynchronous image retrieval libraries out there. E.g. since you are already using Alamofire, I would suggest you consider AlamofireImage. This offers nice asynchronous image retrieval mechanisms, nice UIImageView extensions, the ability to cancel requests that are no longer needed, image caching, etc.
But proper asynchronous image retrieval for a table view is a non-trivial problem. An image processing library like AlamofireImage, KingFisher, SDWebImage, etc., can simplify this process for you.
Related
I am very new to iOS development (with emphasis on very). I think I have grasped simple table views in Xcode without calling on a database, and I also think I understand the basics of how to call data from Firestore, but I cannot for the life of me figure out how to populate my TableView with data from Firestore.
The Firestore collection I want to populate with is called "articles", where each doc represents an article I want to display in a cell. Each doc has this structure of data:
imageURL: https://someurl.com
title: 5 places you don't want to miss
I have created a UITableView with a UITableViewCell inside it in Storyboard, where the TableViewCell's ContentView contains an ImageView for the 'imageURL' data in Firestore and a Label for the 'title' data in Firetore.
The UITableView in Storyboard is linked to ArtiklerTableViewController.swift.
Likewise is the UITableViewCell linked to ArtiklerCell.swift.
The two Swift files look like this now:
ArtiklerTableViewController.swift
class ArtiklerTableViewController: UITableViewController {
#IBOutlet var artiklerTableView: UITableView!
var artiklerArray: [String] = []
var documents: [DocumentSnapshot] = []
var db: Firestore!
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
configureTableView()
loadData()
func configureTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(ArtiklerCell.self, forCellReuseIdentifier: "ArtiklerCell")
// remove separators for empty cells
tableView.tableFooterView = UIView()
// remove separators from cells
tableView.separatorStyle = .none
}
func loadData() {
db.collection("articles").getDocuments() { (QuerySnapshot, err) in
if let err = err {
print("Error getting documents : \(err)")
}
else {
for document in QuerySnapshot!.documents {
let documentID = document.documentID
let artiklerImageView = document.get("imageURL") as! URL
let artiklerTitleLabel = document.get("title") as! String
print(artiklerImageView, artiklerTitleLabel, documentID)
}
self.tableView.reloadData()
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Tableview setup \(artiklerArray.count)")
return artiklerArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ArtiklerCell", for: indexPath) as! ArtiklerCell
let artikler = artiklerArray[indexPath.row]
print("Array is populated \(artiklerArray)")
return cell
}
}
ArtiklerCell.swift
import UIKit
import Firebase
class ArtiklerCell: UITableViewCell {
#IBOutlet weak var artiklerImageView: UIImageView!
#IBOutlet weak var artiklerTitleLabel: UILabel!
var db: Firestore!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
db = Firestore.firestore()
addSubview(artiklerImageView)
addSubview(artiklerTitleLabel)
configureImageView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureImageView() {
artiklerImageView.layer.cornerRadius = 20
artiklerImageView.clipsToBounds = true
}
}
When I try to run the app, I get an error message from the ArtiklerTableViewController.swift regarding the line let artikler = artiklerArray[indexPath.row] in the cellForRowAt function, saying 'Initialization of immutable value 'artikler' was never used; consider replacing with assignment to '_' or removing it'.
I see that this error message makes sense, but I have absolutely no idea what I should do instead.
Pardon my extreme lack of knowledge! I have spent many days now trying to look for the answers I need online without finding a solution. I think I am too inexperienced to correctly search for and absorb the necessary knowledge for this problem.
Any answer will be immensely appreciated!
Thanks in advance from a desperate girl who doesn't want to give up on learning iOS dev as I go through building an app.
You already have the strings in an array and got the artikler corresponding to the row of the cell, now you just need to set the title and the image. Also, you need to append each element to the array before reloading.
func loadData() {
db.collection("articles").getDocuments() { (QuerySnapshot, err) in
if let err = err {
print("Error getting documents : \(err)")
}
else {
for document in QuerySnapshot!.documents {
let documentID = document.documentID
let artiklerImageView = document.get("imageURL") as! URL
let artiklerTitleLabel = document.get("title") as! String
self.artiklerArray.append(artiklerTitleLabel)
}
self.tableView.reloadData()
}
}
}
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ArtiklerCell", for: indexPath) as! ArtiklerCell
let artikler = artiklerArray[indexPath.row]
cell.artiklerTitleLabel.text = artikler
return cell
}
I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}
When I run my app on the simulator the images that I load via a http request don't have proper sizing and bounds clip. I want them to a size 60x60 with a round shape, but instead they scale to fit the UITableViewCell kinda randomly but after I scroll up and down they remain fixed but still to big, I don't know what causes this neither do I know how to fix it, I'm new to iOS.I will post a screenshot with my UIImageView in Table Cell and with the effect that it has when I first run the app and my View Controller class.
I have tried to mess with the constraints, set fixed width and height constraint on the UIImageView but with no result.
I also tried to disable subview auto resize from the cell view but with no result.
This is the effect,this happens before I start scrolling:
This happens after I scroll up and down,the clipping on bounds returns to normal but the size is still to big:
This is my storyboard with the cell image view:
And this is my ViewController.swift class :
//
// ViewController.swift
// TopDevelopers
//
// Created by Eduard Valentin on 12/04/2018.
// Copyright © 2018 Eduard Valentin. All rights reserved.
//
import UIKit
import Alamofire
import Foundation
struct UserInfo {
var name:String
var imageURL:String
var imageView:UIImageView
init(newName: String, newImageURL:String, newImageView: UIImageView) {
self.name = newName
self.imageURL = newImageURL
self.imageView = newImageView
}
}
class ViewController: UIViewController,UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var tableView: UITableView!
var users:[UserInfo] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// GET the data from the stackexchange api
let param: Parameters = [
"order": "desc",
"max" : 10,
"sort" : "reputation",
"site" : "stackoverflow"
]
Alamofire.request("https://api.stackexchange.com/2.2/users", method: .get, parameters: param).responseJSON { (response) -> (Void) in
if let json = response.result.value {
// we got a result
/* I know this is a bit ugly */
let json1 = json as! [String:AnyObject]
let usersInfoFromJSON = json1["items"] as! NSArray // remember to cast it as NSDictionary
for userInfo in usersInfoFromJSON {
let userDict = userInfo as! NSDictionary
Alamofire.request(userDict["profile_image"] as! String).responseData { (response) in
if response.error == nil {
print(response.result)
// Show the downloaded image:
if let data = response.data {
let imageView = UIImageView()
imageView.image = UIImage(data: data)
self.users.append(UserInfo(newName: userDict["display_name"] as! String,
newImageURL: userDict["profile_image"] as! String,newImageView: imageView))
self.tableView.reloadData()
}
}
}
}
}
}
}
#available(iOS 2.0, *)
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.users.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
#available(iOS 2.0, *)
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
cell.cellImageView.image = self.users[indexPath.row].imageView.image
cell.cellImageView.layer.cornerRadius = (cell.cellImageView.layer.frame.height / 2)
cell.cellLabel.text = self.users[indexPath.row].name
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
EDIT 1:
I also tried to set content mode to "scale to fit", "aspect fit" still the same results.
EDIT 2: Ok, I solved it by just deleting almost everything and doing it all over again but this time I did set the option for the "Suggested constraints", also I used xib's for the cells and everything is normal now, I still don't know what caused it.
Set clipsToBounds property to true, and set frames according to cellImageView's frame rather cellImageView.layer's frame:
cell.cellImageView.clipsToBounds = true
cell.cellImageView.layer.masksToBounds = true
cell.cellImageView.contentMode = .scaleAspectFit
cell.cellImageView.layer.cornerRadius = cell.cellImageView.frame.height / 2
And try to add UIImage in your struct rather than UIImageView. And use in cellForRowAtIndexPath as:
cell.cellImageView.image = self.users[indexPath.row].image
In your case do not use CustomCell, because basic tableviewcell provide default UIImageView and UILable
Update inside cellForRowAt function with below code
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourCellIndentifier")
cell.imageView?.contentMode = .scaleAspectFit
cell.imageView?.image = self.users[indexPath.row].imageView.image
cell.imageView?.layer.cornerRadius = cell.imageView?.frame.width / 2
cell.imageView?.layer.masksToBounds = true
return cell
}
I have a table view (controller: MetricsViewController) which gets updated from a CoreData database. I have used prototype cells (MetricsViewCell) which I have customized for my needs. It contains a segmented control, a UIView (metricsChart, which is used to display a chart - animatedCircle), and some UILabels.
MetricsViewCell:
class MetricsViewCell: UITableViewCell {
var delegate: SelectSegmentedControl?
var animatedCircle: AnimatedCircle?
#IBOutlet weak var percentageCorrect: UILabel!
#IBOutlet weak var totalPlay: UILabel!
#IBOutlet weak var metricsChart: UIView! {
didSet {
animatedCircle = AnimatedCircle(frame: metricsChart.bounds)
}
}
#IBOutlet weak var recommendationLabel: UILabel!
#IBOutlet weak var objectType: UISegmentedControl!
#IBAction func displayObjectType(_ sender: UISegmentedControl) {
delegate?.tapped(cell: self)
}
}
protocol SelectSegmentedControl {
func tapped(cell: MetricsViewCell)
}
MetricsViewController:
class MetricsViewController: FetchedResultsTableViewController, SelectSegmentedControl {
func tapped(cell: MetricsViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
var container: NSPersistentContainer? = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer { didSet { updateUI() } }
private var fetchedResultsController: NSFetchedResultsController<Object>?
private func updateUI() {
if let context = container?.viewContext {
let request: NSFetchRequest<Object> = Object.fetchRequest()
request.sortDescriptors = []
fetchedResultsController = NSFetchedResultsController<Object>(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "game.gameIndex",
cacheName: nil)
try? fetchedResultsController?.performFetch()
tableView.reloadData()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Object Cell", for: indexPath)
if let object = fetchedResultsController?.object(at: indexPath) {
if let objectCell = cell as? MetricsViewCell {
objectCell.delegate = self
let request: NSFetchRequest<Object> = Object.fetchRequest()
...
...
}
}
}
return cell
}
When a user selects one of the segments in a certain section's segmented control, MetricsViewController should reload the data in that particular row. (There are two sections with one row each). Hence, I've defined a protocol in MetricsViewCell to inform inform my controller on user action.
Data is being updated using FetchedResultsTableViewController - which basically acts as a delegate between CoreData and TableView. Everything is fine with that, meaning I am getting the correct data into my TableView.
There are two issues:
I have to tap segmented control's segment twice to reload the data in the row where segmented control was tapped.
The table scrolls back up and then down every time a segment from segmented control is selected.
Help would be very much appreciated. I've depended on this community for a lot of issues I've faced during the development and am thankful already :)
For example, in Animal Recognition section, I have to hit "Intermediate" two times for its row to be reloaded (If you look closely, the first time I hit Intermediate, it gets selected for a fraction of second, then it goes back to "Basic" or whatever segment was selected first. Second time when I hit intermediate, it goes to Intermediate). Plus, the table scroll up and down, which I don't want.
Edit: Added more context around my usage of CoreData and persistent container.
Instead of using indexPathForRow(at: <#T##CGPoint#>) function to get the indexPath object of cell you can directly use indexPath(for: <#T##UITableViewCell#>) as you are receiving the cell object to func tapped(cell: MetricsViewCell) {} and try to update your data on the UI always in main thready as below.
func tapped(cell: MetricsViewCell) {
if let lIndexPath = table.indexPath(for: <#T##UITableViewCell#>){
DispatchQueue.main.async(execute: {
table.reloadRows(at: lIndexPath, with: .none)
})
}
}
Your UISegmentedControl are reusing [Default behaviour of UITableView].
To avoid that, keep dictionary for getting and storing values.
Another thing, try outlet connection as Action for UISegmentedControl in UIViewController itself, instead of your UITableViewCell
The below code will not reload your tableview when you tap UISegmentedControl . You can avoid, delegates call too.
Below codes are basic demo for UISegmentedControl. Do customise as per your need.
var segmentDict = [Int : Int]()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...29 // number of rows count
{
segmentDict[i] = 0 //DEFAULT SELECTED SEGMENTS
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SOTableViewCell
cell.mySegment.selectedSegmentIndex = segmentDict[indexPath.row]!
cell.selectionStyle = .none
return cell
}
#IBAction func mySegmentAcn(_ sender: UISegmentedControl) {
let cellPosition = sender.convert(CGPoint.zero, to: tblVw)
let indPath = tblVw.indexPathForRow(at: cellPosition)
segmentDict[(indPath?.row)!] = sender.selectedSegmentIndex
print("Sender.tag ", indPath)
}
I have a list of reddit posts that I want to display the thumbnail of, if it exists. I have it functioning, but it's very buggy. There are 2 main issues:
Images resize on tap
Images shuffle on scroll
This is the code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Post", forIndexPath: indexPath) as UITableViewCell
let post = swarm.posts[indexPath.row]
cell.textLabel!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.imageView!.image = UIImage(named: "first.imageset")
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in // code is at bottom, this just drys things up
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let originalCell = tableView.cellForRowAtIndexPath(indexPath) {
originalCell.imageView?.image = image
originalCell.imageView?.frame = CGRectMake(5,5,35,35)
}
})
}
}
return cell
}
This is the app when it loads up - looks like everything is working:
Then if I tap on an image (even when you scroll) it resizes:
And if you scroll up and down, the pictures get all screwy (look at the middle post - Generics fun):
What am I doing wrong?
** Pictures and Titles are pulled from reddit, not generated by me **
EDIT: FetchAsync class as promised:
class FetchAsync {
var url: String
var callback: (NSData?) -> ()
init(url: String, callback: (NSData?) -> ()) {
self.url = url
self.callback = callback
self.fetch()
}
func fetch() {
var imageRequest: NSURLRequest = NSURLRequest(URL: NSURL(string: self.url)!)
NSURLConnection.sendAsynchronousRequest(imageRequest,
queue: NSOperationQueue.mainQueue(),
completionHandler: { response, data, error in
if(error == nil) {
self.callback(data)
} else {
self.callback(nil)
}
})
callback(nil)
}
}
Unfortunately, this seems to be a limitation of the "Basic" table view cell. What I ended up doing was creating a custom TableViewCell. A relied on a tutorial by Ray Wenderlich that can be found here: http://www.raywenderlich.com/68112/video-tutorial-table-views-custom-cells
It's a bit of a bummer since the code is so trivial, but I guess on the bright side that means it's a 'simple' solution.
My final code:
PostCell.swift (all scaffolded code)
import UIKit
class PostCell: UITableViewCell {
#IBOutlet weak var thumb: UIImageView!
#IBOutlet weak var title: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
PostsController.swift
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PostCell", forIndexPath: indexPath) as PostCell
let post = swarm.posts[indexPath.row]
cell.title!.text = post.title
if(post.thumb? != nil && post.thumb! != "self") {
cell.thumb!.image = UIImage(named: "first.imageset")
cell.thumb!.contentMode = .ScaleAspectFit
var image = self.imageCache[post.thumb!]
if(image == nil) {
FetchAsync(url: post.thumb!) { data in
if(data? != nil) {
image = UIImage(data: data!)
self.imageCache[post.thumb!] = image
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
} else {
dispatch_async(dispatch_get_main_queue(), {
if let postCell = tableView.cellForRowAtIndexPath(indexPath) as? PostCell {
postCell.thumb!.image = image
}
})
}
}
return cell
}
And my measly storyboard:
I'm not sure the best way to do this, but here a couple of solutions:
Use AFNetworking, like everyone else does. It has the idea of a place holder image, async downloading of the replacement image, and smart caching. Install using cocoa pods, make a bridging file with #import "UIImageView+AFNetworking.h"
Create two different types of cells. Before grabbing a cell with dequeReusableCell... in your cellForRowAtIndexPath, check if it's expanded. If expanded, return and populate an expanded cell otherwise return and populated an unexpanded cell. The cell is usually expanded if it is the 'selected' cell.
Your mileage may vary
it is a huge mistake to call tableView.cellForRowAtIndexPath from within UITableViewDataSource's implementation of tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell. Instead when the async fetch of the thumb image is completed, update your model with the image, then request tableView to reloadRows for that specific cell's indexPath. Let your data source determine the correct indexPath. If the cell is offscreen by the time the image download is complete there will be no performance impact. And of course reloadRows on the main thread.