1

I want to implement a method that looks something like this:

setCellHeightForIndexPath(someIndexPath, 80)

and then the table view cell at that index path will suddenly have a height of 80.

The reason I want to do this is because I want the height of the cell to be set to the height of the web view's content after it has finished loading the HTML. Since I can only get the web view's content size after it has finished loading, I can't just set the cell height right away.

See this question for more info.

So in the webViewDidFinishLoad method, I can just get the web view's content height, set the web view's height to that, and call this method to set the cell's height.

It seems like that the cell height can only change when heightForRowAtIndexPath is called. I think the method would use a similar approach as my answer to this question. I think I need to store an array of heights maybe? I just can't think of a way to implement this!

How can I implement such a method?

Note: don't tell me this is not possible. In the Instagram app, I can see different images that have different heights fit perfectly in a table view cell. And those images are similar to my web views. They both need time to load.

EDIT:

Let me show some of my attempts at this:

var results: [Entry] = []
var cellHeights: [CGFloat] = []
var webViews: [UIWebView] = []

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return results.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("resultCell")
    let webView = cell!.contentView.viewWithTag(1) as! UIWebView
    webView.loadHTMLString(results[indexPath.row].htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)
    webView.delegate = self
    webView.scrollView.scrollEnabled = false
    webViews.append(webView)
    cellHeights.append(400)

    webView.stringByEvaluatingJavaScriptFromString("highlightSearch(\"day\")")

    return cell!
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
}

func webViewDidFinishLoad(webView: UIWebView) {
    let height = CGFloat(webView.stringByEvaluatingJavaScriptFromString("document.height")!.toFloat()!)
    webView.frame = CGRect(origin: webView.frame.origin, size: CGSizeMake(webView.frame.width, height))
    print(height)
    if let index = webViews.indexesOf(webView).first {
        cellHeights[index] = height
        tableView.beginUpdates()
        tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .None)
        tableView.endUpdates()
    }
}

results is the stuff that I want to show in the web views. cellHeights is used to store the height of each cell. I put all the web views into the webViews array so I can call indexOf in webViewDidFinishLoad to identify which web view is loaded.

EDIT:

So I wrote this code in my table view controller with reference to Andre's answer:

class SearchResultsController: UITableViewController, UIWebViewDelegate {
    var entries: [Entry] = []
    lazy var results: [Result] = {
        return self.entries.map { Result(entry: $0) }
    }()
    var cellHeights: [CGFloat] = []

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return results.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let result = results[indexPath.section]
        var cell = result.cell
        if cell == nil {
            print("cellForRow called")

            cell = tableView.dequeueReusableCellWithIdentifier("resultCell") as! ResultCell
            cell.webView.delegate = self
            print(cell == nil)

            print("loading \(result.entry.title)...")
            cell.webView.loadHTMLString(result.entry.htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)

            result.cell = cell
        }

        return cell
    }

    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.estimatedRowHeight = 169
        tableView.rowHeight = UITableViewAutomaticDimension

        tableView.tableFooterView = UIView()
    }

    func webViewDidFinishLoad(webView: UIWebView) {
        print("didFinishLoad called")

        if webView.loading {
            return
        }

        guard let cell = webView.superview?.superview as? ResultCell else {
            print("could not get cell")
            return
        }

        guard let index = results.map({$0.cell}).indexOf(cell) else {
            print("could not get index")
            return
        }

        let result = results[index]
        print("finished loading \(result.entry.title)...")

        guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
            print("could not get heightString")
            return
        }

        guard let contentHeight = Float(heightString) else {
            print("could not convert heightString")
            return
        }

        cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
        tableView.beginUpdates()
        tableView.endUpdates()
    }

}

class ResultCell: UITableViewCell {
    @IBOutlet weak var webView: UIWebView!
    @IBOutlet weak var webViewHeightConstraint: NSLayoutConstraint!
}

class Result {
    let entry: Entry
    var contentHeight: Float?
    var cell: ResultCell!

    init(entry: Entry) {
        self.entry = entry
    }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • do you have `heightForRowAtIndexPath` implemented? or do you use automatic cell height calculation by setting up constraints? – André Slotta Jun 18 '16 at 09:29
  • I tried to use automatic cell height with constraints before, like I said in the previous question, it doesn't work, The table cells stays at the height of 44 @AndréSlotta – Sweeper Jun 18 '16 at 09:30

3 Answers3

2

You cannot "push" the new cell height onto a table view. Instead, you need to make table view "pull" the new height from your heightForRowAtIndexPath, and be ready to supply the new height.

When the cell load finishes for row r, you need to update your model in such a way that it knows the new height of row r. After that you need to tell your table view to reload itself, like this:

tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
tableView.endUpdates()

This will start the process of updating your cell. heightForRowAtIndexPath will be called. Your code will return the new height. After that cellForRowAtIndexPath will be called. Your code should be prepared to return the cell that has finished loading, without initiating a new data load.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • (I think) I did what you told me. But a web view that should display a very long content disappears when I run the app. and the console says: `WebKit discarded an uncaught exception in the webView:didFinishLoadForFrame: delegate: attempt to delete row 7 from section 0 which only contains 4 rows before the update` – Sweeper Jun 18 '16 at 09:56
  • @Sweeper One very suspicious thing that happens in your code is that you keep web views from different cells in `webViews` array outside your table view. This is incorrect, because the order in which `cellForRowAtIndexPath` is called for different cells is, essentially, random. You can use the `tag` property of web view itself to store its row in the table. Set `webView.tag = indexPath.row` in `cellForRowAtIndexPath`. Now instead of `webViews.indexesOf(webView).first` you would be able to write `webView.tag` in `webViewDidFinishLoad `. – Sergey Kalinichenko Jun 18 '16 at 10:10
  • Okay, I'll try that. – Sweeper Jun 18 '16 at 10:11
  • Now my web views seem to be out of order. Actually their order is constantly changing. For example, the first cell, just for a fraction of a second, displays the content that the second cell should display. Then it changes back to its original content, but only for a fraction of a second before it changes back to the second cell's content But the heights seem to be working though. – Sweeper Jun 18 '16 at 10:17
  • @Sweeper I see what's going on - you already use `viewWithTag`, so using tag is not going to work (your code `cell!.contentView.viewWithTag(1) as! UIWebView` will stop working). Can you make a custom class for your cell and add `IBOutlet` for `UIWebView` so that you don't have to use `tag` to search for subview on the cell? – Sergey Kalinichenko Jun 18 '16 at 10:35
  • I actually noticed that before. I changed it to `cell!.contentView.subViews.first! as! UIWebView`. And it doesn't work. Since I will have a few of these cells, how can I use an outlet? I can't use multiple outlets because I don't know the number of cells to display at compile time. – Sweeper Jun 18 '16 at 10:40
  • @Sweeper There are several good tutorials explaining creation of custom cells - for example, [this one](https://www.weheartswift.com/swifting-around/). The tutorial creates a cell visually, adds a custom class with iboutlets, and connects outlets to the class. – Sergey Kalinichenko Jun 18 '16 at 10:47
1

i tried implementing it by using automatic autolayout and automatic cell height calculation.

maybe it helps to point you into the right direction: https://github.com/andreslotta/WebViewCellHeight

just an excerpt:

func webViewDidFinishLoad(webView: UIWebView) {
    if webView.loading {
        return
    }

    guard let cell = webView.superview?.superview as? WebViewCell else {
        print("could not get cell")
        return
    }

    guard let index = websites.map({$0.cell}).indexOf(cell) else {
        print("could not get index")
        return
    }

    // get website
    let website = websites[index]
    print("finished loading \(website.urlString)...")

    // get contentheight from webview
    guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
        print("could not get heightString")
        return
    }

    guard let contentHeight = Float(heightString) else {
        print("could not convert heightString")
        return
    }

    cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
    tableView.beginUpdates()
    tableView.endUpdates()
}
André Slotta
  • 13,774
  • 2
  • 22
  • 34
  • Oh! I ran your project and this is exactly what I wanted! Let me examine your code and study how you did it. +1 – Sweeper Jun 18 '16 at 11:35
  • feel free to ask if something is unclear. glad i could help :) – André Slotta Jun 18 '16 at 11:35
  • So I renamed your `Website` to `Result`, `WebViewCell` to `ResultCell` and it says that `$0.cell` is `nil` in the `map` method there. It seems that `webViewDidFinishLoad` is called before `cellForRowAtIndexPath`. Why? This doesn't make sense at all. – Sweeper Jun 18 '16 at 12:06
  • that sounds strange indeed :) can you maybe share your code with me? – André Slotta Jun 18 '16 at 12:08
  • I found the reason! It only works on iPhone 6s Plus! I run your code on iPhone 5 and the same error occurred! Any idea why? – Sweeper Jun 18 '16 at 13:08
  • After some more testing I found out that your code only works when there's a small amount of data. Try adding some URLs into the `websites` array and you'll see it behaving weirdly. – Sweeper Jun 18 '16 at 13:49
  • think i have to do some further investigation when finding time. was just a quick solution to point you into a direction... :) – André Slotta Jun 19 '16 at 08:29
0

You can implement like this

Take one global CGFloat for height and one indexpath now when you need to change height set both values and use

[self.yourTableview beginUpdate] [self.yourTableview endUpdate]

will update your cell

and in cellForRowAtIndexPath you should use dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *) will make sure you got updated cell every time

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

     if (indexPath == yourIndexpath) {

        return gloablVariable;
    } else {
        return defauleHeight
    }
 }

Hope it helps

Prashant Tukadiya
  • 15,838
  • 4
  • 62
  • 98