I need to programmatically add two different types of cells to my UITableView. The content of these cells is not known until runtime. The first type is an HTML formatted string and the second is an image. There can be any combination of these cells and they can occur in any order.
In the IB, I've set up two prototype cells, one with the id 'htmlCell' and the other 'imageCell'. Here is the relevant code:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "htmlCell")
let thisSection = self.sections[indexPath.row]
if thisSection.type! == "html" {
let htmlString = thisSection.text
let htmlHeight = contentHeights[indexPath.row]
let webView:UIWebView = UIWebView(frame: CGRect(x:0, y:0, width:cell.frame.size.width, height:htmlHeight))
cell.addSubview(webView)
webView.tag = indexPath.row
webView.scrollView.isScrollEnabled = false
webView.isUserInteractionEnabled = false
webView.delegate = self
webView.loadHTMLString(htmlString!, baseURL: nil)
} else if thisSection.type! == "image" {
cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "imageCell")
let imageName = "logo"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width-20, height: 200)
cell.addSubview(imageView)
return cell
}
return cell
}
func webViewDidFinishLoad(_ webView: UIWebView) {
if (contentHeights[webView.tag] != 0.0) {
// we already know height, no need to reload cell
return
}
let strHeight = webView.stringByEvaluatingJavaScript(from: "document.body.scrollHeight")
contentHeights[webView.tag] = CGFloat(Float(strHeight!)!)
tableView.reloadRows(at: [NSIndexPath(item: webView.tag, section: 0) as IndexPath], with: UITableViewRowAnimation.automatic)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return self.contentHeights[indexPath.row]
}
The content loads, but it's a bit of mess. Here is a screenshot to provide an example. In this case, the image should appear between the two webviews. But as you can see, the image lands directly on top of the second webview, with the two top borders aligned.
Also, when I click on the image, it completely disappears. I'm assuming that it goes behind the second webview. Just a guess.
The CGRects that I am using have somewhat arbitrary widths and heights at the moment. I suspect that this is where part of the issue lies. Finally, the webViewDidFinishLoad always returns a height of 667.0 regardless of the true height of the webview content.
Anyone know how I can make these views land in the correct sequence? Thank you.
There are many potential issues here, let's tackle a few of them and see if that get's you going :)
If you defined prototype cells in IB and your tableView is properly linked to the xib or storyboard file, then you should dequeue cells instead of construct them.
Try this:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell //declare but don't assign yet. Use let because after we assign once, we wont be reassigning.
...
if thisSection.type! == "html" {
cell = tableView.dequeueReusableCell(withIdentifier: "htmlCell", for: indexPath)
...
} else if thisSection.type! == "image" {
cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath)
...
}
return cell
}
Notice, we declare and return cell at the top level scope and assign the cell only when we know what type it will be. Now that you are dequeueing, the cells will be reused when appropriate and reduce the number of allocation made when a user scrolls making your app zippier.
Next, since you are adding subviews to your cells, you'll need to clear the subviews each time you dequeue a cell. This requires a bit of additional code but will make things a lot cleaner in the long run.
After each call to dequeueReusableCell... add:
cell.contentView.subviews.forEach({ $0.removeFromSuperview() })
So far so good. You've got the cells dequeuing cleanly, but now we need to make sure we setup the cells properly.
First, the html cell:
Remove the line:
let htmlHeight = contentHeights[indexPath.row]
The tableView is ultimately responsible for sizing the cells and the content will take up the full size of the cell. Replace the next 2 lines with this:
let webView = UIWebView(frame: cell.contentView.bounds)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
cell.contentView.addSubview(webView)
The autoresizing mask makes sure that if the cell's content view size changes, the webview will be resized as well. Since the content view is managed by iOS, the webview will always be the width of the tableview and the height will be whatever you provide in the delegate method heightForRow. Notice also, we never add subviews directly on to the cell, always on to it's contentView.
Next, we'll do the same for the imageCell:
let image = UIImage(named: "logo")
let imageView = UIImageView(frame: cell.contentView.bounds)
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
imageView.contentMode = .aspectFit //this makes sure the image take ups the full size of the image view, but no more. and this keeps the image's aspect ratio. Adjust this to other content modes if necessary.
imageView.image = image
cell.contentView.addSubview(imageView)
Ok, now we have cells that are being setup and leaving the sizing out of the equation. So let's get sizing working in the right place:
The biggest challenge that you'll face in building this is getting the cell heights right. Since the images are in your bundle, you'll know their size as soon as you instantiate the UIImage, however we don't want to do this more than absolutely necessary. You might want to come up with a more performant way of doing this later, but for now we'll create a UIImage and get it's height:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if thisSection.type! == "html" {
return self.contentHeights[indexPath.row]
} else if thisSection.type! == "image" {
let image = UIImage(named: "logo")
let imageHeight = (image.size.height * tableView.bounds.size.width) / image.size.width // The proportional size of the image stretched to the table view's width
return imageHeight
}
}
Your method for calculating the web view height and reloading cell might work, but it might appear clunky to the user. My suggestion at this point would be to return a fixed height for the web content. When the user taps a web cell, you can push a new view controller with a web view that can display all the content. Otherwise you can try having all of your html generate web content of roughly the same size.
Ok, this was long but hopefully gets you off to a good start :) I'm happy to follow up if you need more clarification.
Related
update
now images displays as they are loaded, but there still a problem I can't solve.
The images overlaps the content of the cell even the three views are in a vertical stack (first is the image, the next two are label views).
I wonder how can a view overlap other views in a stackview
now my talbe view delegate method looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
Alamofire.request(url).responseImage(completionHandler: { (response) in
if let image = response.result.value {
cell.imageView?.image = image
cell.setNeedsLayout()
}
})
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
I'm very new to iOS. I've already tutorials and watched video courses and now I would like to implement my very first app.
I try to read a feed and display the content's title, lead and image (if it has any).
I defined a cell, with an UIMageVeiw, and two UILables, at last I embedded them into a vertical StackView (I set distribution to Fill).
Everything works well, but images don't display automatically, just if I click on the cell, or sroll the TableVeiw.
And even if I set the image view to Aspect Fit (and embedded it into the top tof the vertical stack view), when the image displays it keeps it's original size and overlaps the cell content.
I've trying to find out what I do wrong for two days, but I can't solve it.
I try display data like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell!
if let contents = contents {
let content = contents[indexPath.row]
if let imageUrl = content.imageUrl, let url = URL(string: imageUrl) {
cell = tableView.dequeueReusableCell(withIdentifier: "ImageContentRow", for: indexPath) as! ContentWithImageTableViewCell
cell.imageView?.af_setImage(withURL: url)
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "ContentRow", for: indexPath) as! ContentTableViewCell
}
cell.contentTitleVeiw.text = content.title
cell.contentLeadView.text = content.lead
}
return cell!
}
The image view cell's view hierarchy looks like this:
My list looks like this after I start my app and data displayed:
After I click on a cell that should display an image or scroll it out and back the result is this:
At least this is my list view controller with the two prototype cells
(one for contents with image, one for without image)
You need to notify the tableView that the image was loaded and that it has to redraw the cell.
Try changing
cell.imageView?.af_setImage(withURL: url)
to
cell.imageView?.af_setImage(withURL: url, completion: { (_) in
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
})
Take a look at my answer here - in the end you might need to explicitly set height on the imageView.
I am using the default UITableViewCell which comes with UIImageView, titleLabel and also a description Label. I want to have a fixed size for my UIImageView so all the images are of the same size. At present it looks like this:
The row height for the cell is "Automatic" and here are the properties of the UITableView.
How can I make my UIImageView fixed size?
The images are downloaded asynchronously as shown below:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let dish = self.dishes[indexPath.row]
cell.textLabel?.text = dish.title
cell.imageView?.image = UIImage(named: "placeholder")
cell.detailTextLabel?.text = dish.description
cell.imageView?.setImageFor(url: URL(string: dish.imageURL)!) { image in
cell.imageView?.image = image
cell.imageView?.translatesAutoresizingMaskIntoConstraints = false
cell.imageView?.widthAnchor.constraint(equalToConstant: 50)
cell.imageView?.heightAnchor.constraint(equalToConstant: 50)
cell.setNeedsLayout()
}
return cell
}
UIImageView+Additions.swift
func setImageFor(url :URL, completion: #escaping (UIImage) -> ()) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: url)
if let data = data {
let image = UIImage(data: data)
DispatchQueue.main.async {
completion(image!)
}
}
}
}
You are trying to change the height anchor and width anchor of your UIImageView inside your completion block handler. You should not do that, and if you need to, make sure you are updating your layout in the main thread.
Your UIImageView's top, bottom, leading, and trailing anchor might be equal to the superView, in your case, it's the tableViewCell and given that your UITableViewCells have auto height. These result to unequal size of your UIImageView.
Since you are using interface builder, try to layout your UIImageView like so:
As you can see, I have my fixed width and height of my UIImageView.
Lastly, don't forget to set your UIImageView's contentMode and tick clipToBounds to yes.
I'm going through Stanford's cs193p. Assignment 4 has us create a custom UITableVIewCell and load a picture from the web into a UIImageView inside the cell.
My UIImageView and my Cell have their content mode set to Aspect Fit on the story board.And the ImageView is set on autolayout to be hugging the cell.
And yet when the picture first loads, it will bleed out of the UIImageView. When I click on it, it will correctly aspect fit.
I tried setting the content mode in code just before assigning the image, but that also didn't work. I also tried calling layoutSubviews() and setNeedsLayout right after assigning the image, and while that helps by actually showing the image (as opposed to showing nothing until the user clicks the cell), it still shows in the wrong size until the user clicks it.
This is the code for the cell:
import UIKit
class ImageTableViewCell: UITableViewCell {
#IBOutlet weak var pictureView: UIImageView!
var pictureURL: URL? {
didSet {
fetchImage()
}
}
fileprivate func fetchImage() {
if let url = pictureURL {
pictureView.image = nil
let queue = DispatchQueue(label: "image fetcher", qos: .userInitiated)
queue.async { [weak weakSelf = self] in
do {
let contentsOfURL = try Data(contentsOf: url)
DispatchQueue.main.async {
if url == self.pictureURL {
weakSelf?.pictureView?.image = UIImage(data: contentsOfURL)
weakSelf?.layoutSubviews()
print("loaded")
}
}
} catch let exception {
print(exception.localizedDescription)
}
}
}
}
}
This is the code that loads the cell on its TableViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
switch indexPath.section {
case 0:
cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath)
if let imageCell = cell as? ImageTableViewCell {
imageCell.pictureURL = tweet?.media[indexPath.row].url
// other stuff not programmed yet
}
return cell
The code that gives me the cell's height:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 && tweet != nil {
let media = tweet?.media[indexPath.row]
return tableView.frame.width / CGFloat(media!.aspectRatio)
}
return UITableViewAutomaticDimension
}
I'm sorry for pasting all this code, but I have no idea where the problem is so I'm putting everything I can this might be related.
You should set content mode first and then you should set the frame of your imageview, so once you should try to set content mode in awakeFromNib of tableview subclass or from cellforrowatindexpath before setting image to it!
Or you can set your content mode from interface builder (from storyboard!) - > select your imageview - > fro attribute inspector - > select mode(under view) to Aspect fit
Well, following an answer on reddit, I deleted the table view controller and remade it, setting all the outlets again. It worked, I guess it was a problem in Xcode?
So if you're having a problem like this, try remaking your storyboard.
I have a custom table cell inside a table view, which contains a horizontal stack view of image view and text field... The image view mode is ScaleAspectFit
When the view loads initially, the views are arranged as they should be, but when a cell is touched or the table view is scrolled, the image view resizes unexpectedly
Here are screenshots and my code
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ImagesTableViewCell", forIndexPath: indexPath) as! ImagesTableViewCell
// Configure the cell...
let pic = self.patient!.images[indexPath.row]
cell.imageViewPic.clipsToBounds = true
cell.imageViewPic.contentMode = .ScaleAspectFit
cell.imageViewPic.image = getImage(pic.imagePath)
cell.labelPic.text = pic.caption
cell.imageViewPic.tag = indexPath.row
cell.labelPic.tag = indexPath.row
return cell
}
I tried all Content Modes with no benefit
Edit2: here is getImage() function code as requested
func getImage(filename: String) -> UIImage {
let path = getDocumentsDirectory().stringByAppendingPathComponent(filename)
let image = UIImage(contentsOfFile: path)
return image!
}
Set the clipsToBounds property of UIImageView to true.
Like
set cell.imageViewPic.clipsToBounds=true
Also check the constraint of the imageview. It should be center vertical align,leading, fixed width and fixed height.
try below steps it may can help you.
render ImageView with fixe width and height
make clipSubviews = YES. in story board OR cell.imageViewPic.clipsToBounds = YES;
Using Xcode 7.3 here, wanting to adjust my UITextView margins, but it's not working on all instances when my view loads. Only when scrolling table rows off-screen, then back on, do the UITextViews correct themselves.
I'm using this code in a configureCell function for a custom PostCell:
self.movieSummary.setContentOffset(CGPointMake(5.0, 10.0), animated: false)
...but it only gets applied to the top row's UITextView on first load!?
Here are two screenshots which show the problem with the UITextView highlighted in red...
1) On first load: Only the top row is OK. Other rows are misaligned.
2) After scrolling down and back up: All rows are correctly aligned.
How can I force all the rows/cells/UITextViews to update on first load?
EDIT 1: Here's my code for cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = moviePosts[indexPath.row]
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell {
cell.request?.cancel()
var posterImg: UIImage?
if let url = post.moviePosterImgPath {
posterImg = FeedVC.imageCache.objectForKey(url) as? UIImage
}
var bgImg: UIImage?
if let url = post.movieBgImgPath {
bgImg = FeedVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post, posterImg: posterImg, bgImg: bgImg)
return cell
} else {
return PostCell()
}
}