I've a vertical listing with 7 types of UITableViewCells. One of them consist a UITableView inside the cell.
My requirement is the main tableview should autoresize the cell according to the cell's inner tableview contentSize. That os the inner tableview will show its full length and the scrolling will be off.
This approach working fine, but for cell with tableview only. When I introduce a UIImageView (with async loading image) above inner tableview, the total height of cell is somewhat smaller than the actual height of its contents. And so the inner tableview is getting cut from bottom.
Here is a representation of the bug.
I'm setting the height of UImageView according to the width to scale properly:
if let media = communityPost.media, media != "" {
postImageView.sd_setImage(with: URL(string: media), placeholderImage: UIImage(named: "placeholder"), options: .highPriority) { (image, error, cache, url) in
if let image = image {
let newWidth = self.postImageView.frame.width
let scale = newWidth/image.size.width
let newHeight = image.size.height * scale
if newHeight.isFinite && !newHeight.isNaN && newHeight != 0 {
self.postViewHeightConstraint.constant = newHeight
} else {
self.postViewHeightConstraint.constant = 0
}
if let choices = communityPost.choices {
self.datasource = choices
}
self.tableView.reloadData()
}
}
} else {
self.postViewHeightConstraint.constant = 0
if let choices = communityPost.choices {
datasource = choices
}
tableView.reloadData()
}
And the inner table view is a subclass of UITableView :
class PollTableView: UITableView {
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
override var contentSize: CGSize {
didSet{
self.invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
}
The main table view is set to resize with automaticDimension :
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}
Can't seem to understand what is going wrong. Any help is appreciated.
Thanks.
When the table view is asking you to estimate the row height, you are calling back the table view. Thus you are not providing it with any information it doesn't already have.
The problem is probably with your async loading image, so you should predict the image size and provide the table view with properly estimated row height when the image hasn't loaded yet.
Related
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'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 TableViewController, inside the TableViewCell, I have a UIWebView. I want the UIWebView to display some content from the internet, but I don't want the scroll effect, I want the WebView to have a dynamic height based on the length of the content. In addition, I want the TableViewCell to be able to adjust its cell height dynamically based on the dynamic height of WebView. Is this possible?
This is how I implemented my TableViewController:
class DetailTableViewController: UITableViewController {
var passPost: Posts = Posts()
var author: Author = Author()
override func viewDidLoad() {
super.viewDidLoad()
getAuthor()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("detailCell")
let postImageUrlString = passPost.postThumbnailUrlString
let postImageUrl = NSURL(string: postImageUrlString)
let size = CGSize(width: 414.0, height:212.0 )
let filter = AspectScaledToFillSizeFilter(size: size)
(cell?.contentView.viewWithTag(1) as! UIImageView).af_setImageWithURL(postImageUrl!, filter: filter)
//Set Author Avatar
let authorAvatarUrlString = author.authorAvatarUrlString
let authorAvatarUrl = NSURL(string: authorAvatarUrlString)
//Mark - Give Author Avatar a Round Corner
let filter2 = AspectScaledToFillSizeWithRoundedCornersFilter(size: (cell?.contentView.viewWithTag(2) as! UIImageView).frame.size, radius: 20.0)
(cell?.contentView.viewWithTag(2) as! UIImageView).af_setImageWithURL(authorAvatarUrl!, filter: filter2)
//Set Post Title and Content and so on
(cell?.contentView.viewWithTag(4) as! UILabel).text = passPost.postTitle
(cell?.contentView.viewWithTag(6) as! UIWebView).loadHTMLString("\(passPost.postContent)", baseURL: nil)
for the heightForCellAtIndexPath I did
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! tableViewCell
tableView.rowHeight = cell.theWebView.scrollView.contentSize.height
return tableView.rowHeight
}
This is working fine, except the WebView has a scroll effect, and the height is limited due to the limitation of the TableViewCell. So, how to achieve what I need?
You need to set the height of your cell in the UITableViewDelegate method
tableView(_:heightForRowAt:)
You would do all your calculations on each individual cell height in this method and return it. If you want to display the UIWebView in it's whole without the need to scroll, you should return the height of the UIWebView's scroll view contentView – plus any height for anything else you might want to display in this cell.
Follow the steps:
1) set leading, trailing, top, bottom constraint 0(zero) from webview to tableviewCell
2) Now no need to call HeightForRow method of tableView.
3) in webview delegate method
func webViewDidFinishLoad(webView: UIWebView)
{
var frame = cell.webView.frame
frame.size.height = 1
let fittingSize = cell.webView.sizeThatFits(CGSizeZero)
frame.size = fittingSize
}
4) webview scrollenabled = false
You can return, in heightForRowAtIndexPath:
tableView.rowHeight = UITableViewAutomaticDimension
You also need to have a value setted in estimatedRowHeight, like
tableView.estimatedRowHeight = 85.0
If you have all the constraints defined correctly in the Storyboard (Or programmatically), you shouldn't get any error.
Reference: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html
I am supporting iOS 7 and I am not using autolayout. Is there a way I can have dynamic height for cell labels doing it this way?
Thanks
EDIT:
Here is the code I am using to define a dynamic height in iOS 7, it seems I can get it kinda working with auto layout but it cuts off the last cell at the bottom, it is weird.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
var cell = offScreenCells.objectForKey("gcc") as? GalleryCommentCell
if cell == nil {
cell = commentsTable.dequeueReusableCellWithIdentifier("GalleryCommentCustomCell") as? GalleryCommentCell
offScreenCells.setObject(cell!, forKey: "gcc")
}
let comment :GalleryCommentInfo = commentResults[indexPath.row]
setCellCommentInfo(cell!, data: comment)
cell!.bounds = CGRectMake(0, 0, CGRectGetWidth(commentsTable.bounds), CGRectGetHeight(cell!.bounds))
cell!.setNeedsLayout()
cell!.layoutIfNeeded()
let height = cell!.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
return height + 1
}
func setCellCommentInfo(cell :GalleryCommentCell, data :GalleryCommentInfo) {
cell.commentDate.text = data.galleryCommentDate
cell.comment.text = data.galleryComment
}
In your custom cell implement method like this:
+ (CGFloat)heightForContactName:(NSString *)name
{
CGFloat height = 0.0f;
if (name) {
CGFloat heightForText;
// Calculate text height with `textBoundingRect`...
height += heightForText;
}
return height;
}
I'm building an app in iOS 8.4 with Swift.
I have a UITableView with a custom UITableViewCell that includes a UILabel and UIImageView. This is all fairly straight forward and everything renders fine.
I'm trying to create a parallax effect similar to the one demonstrated in this demo.
I currently have this code in my tableView.cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = self.tableView.dequeueReusableCellWithIdentifier("myitem", forIndexPath: indexPath) as! MixTableViewCell
cell.img.backgroundColor = UIColor.blackColor()
cell.title.text = self.items[indexPath.row]["title"]
cell.img.image = UIImage(named: "Example.png")
// ideally it would be cool to have an extension allowing the following
// cell.img.addParallax(50) // or some other configurable offset
return cell
}
That block exists inside a class that looks like class HomeController: UIViewController, UITableViewDelegate, UITableViewDataSource { ... }
I am also aware that I can listen to scroll events in my class via func scrollViewDidScroll.
Other than that, help is appreciated!
I figured it out! The idea was to do this without implementing any extra libraries especially given the simplicity of the implementation.
First... in the custom table view Cell class, you have to create an wrapper view. You can select your UIImageView in the Prototype cell, then choose Editor > Embed in > View. Drag the two into your Cell as outlets, then set clipToBounds = true for the containing view. (also remember to set the constraints to the same as your image.
class MyCustomCell: UITableViewCell {
#IBOutlet weak var img: UIImageView!
#IBOutlet weak var imgWrapper: UIView!
override func awakeFromNib() {
self.imgWrapper.clipsToBounds = true
}
}
Then in your UITableViewController subclass (or delegate), implement the scrollViewDidScroll — from here you'll continually update the UIImageView's .frame property. See below:
override func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = self.tableView.contentOffset.y
for cell in self.tableView.visibleCells as! [MyCustomCell] {
let x = cell.img.frame.origin.x
let w = cell.img.bounds.width
let h = cell.img.bounds.height
let y = ((offsetY - cell.frame.origin.y) / h) * 25
cell.img.frame = CGRectMake(x, y, w, h)
}
}
See this in action.
I wasn't too happy with #ded's solution requiring a wrapper view, so I came up with another one that uses autolayout and is simple enough.
In the storyboard, you just have to add your imageView and set 4 constraints on the ImageView:
Leading to ContentView (ie Superview) = 0
Trailing to ContentView (ie Superview) = 0
Top Space to ContentView (ie Superview) = 0
ImageView Height (set to 200 here but this is recalculated based on the cell height anyway)
The last two constraints (top and height) need referencing outlets to your custom UITableViewCell (in the above pic, double click on the constraint in the rightmost column, and then Show the connection inspector - the icon is an arrow in a circle)
Your UITableViewCell should look something like this:
class ParallaxTableViewCell: UITableViewCell {
#IBOutlet weak var parallaxImageView: UIImageView!
// MARK: ParallaxCell
#IBOutlet weak var parallaxHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var parallaxTopConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
clipsToBounds = true
parallaxImageView.contentMode = .ScaleAspectFill
parallaxImageView.clipsToBounds = false
}
}
So basically, we tell the image to take as much space as possible, but we clip it to the cell frame.
Now your TableViewController should look like this:
class ParallaxTableViewController: UITableViewController {
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return cellHeight
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! ParallaxTableViewCell
cell.parallaxImageView.image = … // Set your image
cell.parallaxHeightConstraint.constant = parallaxImageHeight
cell.parallaxTopConstraint.constant = parallaxOffsetFor(tableView.contentOffset.y, cell: cell)
return cell
}
// Change the ratio or enter a fixed value, whatever you need
var cellHeight: CGFloat {
return tableView.frame.width * 9 / 16
}
// Just an alias to make the code easier to read
var imageVisibleHeight: CGFloat {
return cellHeight
}
// Change this value to whatever you like (it sets how "fast" the image moves when you scroll)
let parallaxOffsetSpeed: CGFloat = 25
// This just makes sure that whatever the design is, there's enough image to be displayed, I let it up to you to figure out the details, but it's not a magic formula don't worry :)
var parallaxImageHeight: CGFloat {
let maxOffset = (sqrt(pow(cellHeight, 2) + 4 * parallaxOffsetSpeed * tableView.frame.height) - cellHeight) / 2
return imageVisibleHeight + maxOffset
}
// Used when the table dequeues a cell, or when it scrolls
func parallaxOffsetFor(newOffsetY: CGFloat, cell: UITableViewCell) -> CGFloat {
return ((newOffsetY - cell.frame.origin.y) / parallaxImageHeight) * parallaxOffsetSpeed
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = tableView.contentOffset.y
for cell in tableView.visibleCells as! [MyCustomTableViewCell] {
cell.parallaxTopConstraint.constant = parallaxOffsetFor(offsetY, cell: cell)
}
}
}
Notes:
it is important to use tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) and not tableView.dequeueReusableCellWithIdentifier("CellIdentifier"), otherwise the image won't be offset until you start scrolling
So there you have it, parallax UITableViewCells that should work with any layout, and can also be adapted to CollectionViews.
This method works with table view and collection view.
first of all create the cell for the tableview and put the image view in it.
set the image height slightly more than the cell height. if cell height = 160 let the image height be 200 (to make the parallax effect and you can change it accordingly)
put this two variable in your viewController or any class where your tableView delegate is extended
let imageHeight:CGFloat = 150.0
let OffsetSpeed: CGFloat = 25.0
add the following code in the same class
func scrollViewDidScroll(scrollView: UIScrollView) {
// print("inside scroll")
if let visibleCells = seriesTabelView.visibleCells as? [SeriesTableViewCell] {
for parallaxCell in visibleCells {
var yOffset = ((seriesTabelView.contentOffset.y - parallaxCell.frame.origin.y) / imageHeight) * OffsetSpeedTwo
parallaxCell.offset(CGPointMake(0.0, yOffset))
}
}
}
where seriesTabelView is my UItableview
and now lets goto the cell of this tableView and add the following code
func offset(offset: CGPoint) {
posterImage.frame = CGRectOffset(self.posterImage.bounds, offset.x, offset.y)
}
were posterImage is my UIImageView
If you want to implement this to collectionView just change the tableView vairable to your collectionView variable
and thats it. i am not sure if this is the best way. but it works for me. hope it works for you too. and let me know if there is any problem
After combining answers from #ded and #Nycen I came to this solution, which uses embedded view, but changes layout constraint (only one of them):
In Interface Builder embed the image view into a UIView. For that view make [√] Clips to bounds checked in View > Drawing
Add the following constraints from the image to view: left and right, center Vertically, height
Adjust the height constraint so that the image is slightly higher than the view
For the Align Center Y constraint make an outlet into your UITableViewCell
Add this function into your view controller (which is either UITableViewController or UITableViewControllerDelegate)
private static let screenMid = UIScreen.main.bounds.height / 2
private func adjustParallax(for cell: MyTableCell) {
cell.imageCenterYConstraint.constant = -(cell.frame.origin.y - MyViewController.screenMid - self.tableView.contentOffset.y) / 10
}
Note: by editing the magic number 10 you can change how hard the effect will be applied, and by removing the - symbol from equation you can change the effect's direction
Call the function from when the cell is reused:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCellId", for: indexPath) as! MyTableCell
adjustParallax(for: cell)
return cell
}
And also when scroll happens:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
(self.tableView.visibleCells as! [MyTableCell]).forEach { cell in
adjustParallax(for: cell)
}
}