13

Question summary: It crashes when I have a lot of cells in my UITableView when animating the height of a UITableViewCell from a UITextView editing it's text. Using iOS 8 self-sizing-cells.

Long Question: I have successfully implemented so I can dynamically with iOS 8 self-sizing cells enter text into the cells UITextView and change the cells height without losing focus(firstReponder). However, if the tableView is too large (have too many rows) it crashes. Here is my stacktrace:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
*** First throw call stack:
(
0   CoreFoundation                      0x000000010b3b3d85 __exceptionPreprocess + 165
1   libobjc.A.dylib                     0x000000010b9cbdeb objc_exception_throw + 48
2   CoreFoundation                      0x000000010b274cc5 -[__NSArrayM insertObject:atIndex:] + 901
3   UIKit                               0x0000000108b05439 __46-[UITableView _updateWithItems:updateSupport:]_block_invoke1029 + 180
4   UIKit                               0x0000000108a7e838 +[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 582
5   UIKit                               0x0000000108a7ec6d +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] + 105
6   UIKit                               0x0000000108b05048 -[UITableView _updateWithItems:updateSupport:] + 4590
7   UIKit                               0x0000000108afd5a0 -[UITableView _endCellAnimationsWithContext:] + 15360
8   Test RYM                            0x0000000107e6a173 _TFE8Test_RYMCSo11UITableView31reloadDataAnimatedKeepingOffsetfT_T_ + 163
9   Test RYM                            0x0000000107e6a242 _TToFE8Test_RYMCSo11UITableView31reloadDataAnimatedKeepingOffsetfT_T_ + 34
10  Test RYM                            0x0000000107dcda90 _TFC8Test_RYM20AgendaViewController19cellHeightDidUpdatefTCSo11NSIndexPath6heightV12CoreGraphics7CGFloat_T_ + 144
11  Test RYM                            0x0000000107dcdb04 _TToFC8Test_RYM20AgendaViewController19cellHeightDidUpdatefTCSo11NSIndexPath6heightV12CoreGraphics7CGFloat_T_ + 68
12  Test RYM                            0x0000000107e725cb _TFC8Test_RYM27AgendaDecisionTableViewCell20updateTextViewHeightfT_T_ + 907
13  Test RYM                            0x0000000107e731fa _TFC8Test_RYM27AgendaDecisionTableViewCell17textViewDidChangefCSo10UITextViewT_ + 42

And the code that causes it:

// In UITableView extension
func reloadDataAnimatedKeepingOffset() {
    //let offset = contentOffset
    //UIView.setAnimationsEnabled(false)
    beginUpdates()
    endUpdates()
    //UIView.setAnimationsEnabled(true)
    //layoutIfNeeded()
    //contentOffset = offset
}

// In a self-sizing UITableViewCell subclass
func updateTextViewHeight() {
    let size = decisionTextView.bounds.size
    let newSize = decisionTextView.sizeThatFits(CGSize(width: size.width, height: CGFloat.max))
    let newHeight = newSize.height
    if size.height != newHeight {
        textViewHeightConstraint.constant = newHeight
        agendaViewController?.cellHeightDidUpdate(indexPath!, height: newSize.height)
    }
}

// In the ViewController managing the tableView
public func cellHeightDidUpdate(indexPath: NSIndexPath, height: CGFloat) {
    updateHelperAlphas()
    tableView?.reloadDataAnimatedKeepingOffset()
}

It crashes in the call to endUpdates(). I've tried to remove the tableView:estimatedHeightForRowAtIndexPath: method mentioned in UITableView insertRowsAtIndexPaths throwing __NSArrayM insertObject:atIndex:'object cannot be nil' error without success.

It also seems to occur only when the list is long.

Edit: More methods I use:

public func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 48.0
}

public func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    if (section == 0) {
        return 0
    }
    else {
        let currentSectionIsEmpty = sectionIsEmpty(section)
        if ((!isInEditProtocolMode && isProtocolMode) || isPreviousProtocolMode) && currentSectionIsEmpty {
            return 0
        }
        let subSection = sectionHelper.subSectionForSection(section)
        let isProtocolTopSection = isProtocolMode && subSection == 0
        if (isProtocolTopSection) {
            return UITableViewAutomaticDimension
        }
        else {
            return agendaHeaderHeight
        }
    }
}

public func tableView(tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
    if (section == 0) {
        return 0
    }
    else {
        let subSection = sectionHelper.subSectionForSection(section)
        let isProtocolTopSection = isProtocolMode && subSection == 0
        if (isProtocolTopSection) {
            return protocolAgendaHeaderHeight
        }
        else {
            return agendaHeaderHeight
        }
    }
}

public override func viewDidLoad() {
    super.viewDidLoad()
    tableView.rowHeight = UITableViewAutomaticDimension
}

Edit2: This is almost the same problem. tableView crashes on end up with more than 16 items However I cannot remove the estimation of cell height as this breaks my dynamic heights for my self-sizing cellviews.

Edit 3: Tried (from comments below) to use CATransaction.setDisableActions(_) and setContentOffset(_:animated:) without any help. It seems to be not related to this at all as removing all but beginUpdates() and endUpdates() does not help either. reloadDataAnimatedKeepingOffset() seems to be only called once and no other reloadData seems to be called at the same time. Setting estimated height to 1 instead of 0 does not help either. It weirdly shows the section zero header instead (not height 1).

Edit 4: On request here are my numberOrRowsInSection and cellForRowAtIndexPath methods (the are a bit complex):

public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
    let mainSection = sectionHelper.mainSectionForSection(section)
    let subSection = sectionHelper.subSectionForSection(section)


    let isHeaderSection = subSection <= 0
    if isHeaderSection {
        return isProtocolMode ? 0 : cellIdSectionList[0].count
    }

    let rowType = sectionHelper.rowTypeForSection(section)

    let agenda = agendaForSection(section)

    switch rowType {
    case .NoteRow:
        return mainSectionShowingPlaceholderNewNoteCell == mainSection || !agenda.protocolString.isEmpty ? 1 : 0
    case .ActionRow:
        let count = max(0, agenda.actionListCount())
        return count
    case .DecisionRow:
        var count = agenda.decisions.count ?? 0
        count = max(0, count)
        count = indexPathShowingPlaceholderNewDecisionCell?.section == section ? count+1 : count
        return count
    default:
        return 0
    }
}

public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
    let row = indexPath.row
    let section = indexPath.section
    let cellId = cellIdForIndexPath(indexPath)

    let cell = tableView.dequeueReusableCellWithIdentifier(cellId, forIndexPath: indexPath)
    let allowEditing = (isInEditProtocolMode || !isProtocolMode) && !isPreviousProtocolMode

    if let titleCell = cell as? StandardTitleTableViewCell {
        titleCell.setup(agendaForSection(section), meeting:selectedMeeting!, indexPath:indexPath)
        titleCell.delegate = self
    }
    if let descriptionCell = cell as? StandardDescriptionTableViewCell {
        descriptionCell.setup(agendaForSection(section), meeting:selectedMeeting!, indexPath: indexPath, forceExpand: hasExpandedDescriptionCellView)
        descriptionCell.delegate = self
    }

    if let noteCell = cell as? AgendaNotesTableViewCell {
        noteCell.setup(agendaForSection(section), meeting:selectedMeeting!, indexPath: indexPath, allowEditing:allowEditing)
        noteCell.agendaViewController = self
    }

    if let decisionCell = cell as? AgendaDecisionTableViewCell {
        let decisionList = agendaForSection(section).decisions
        let isNewDecisionCell = row >= decisionList.count
        if !isNewDecisionCell {
            let decision = decisionList[row]
            decisionCell.setup(decision, meeting:selectedMeeting!, indexPath: indexPath, allowEditing: allowEditing)
        }
        else {
            decisionCell.setup(newDecisionToAdd!, meeting:selectedMeeting!, indexPath: indexPath, allowEditing: allowEditing)
        }
        decisionCell.agendaViewController = self
    }

    if let actionCell = cell as? StandardTableViewCell,
        let actionList = agendaForSection(section).actionList {
            let action = actionList.actions[row]
            actionCell.setupAsActionListCell(action:action, indexPath: indexPath, delegate: self)
    }

    if let textCell = cell as? MeetingTextTableViewCell {
        var placeholderText = ""
        if indexPathIsActionTextPlaceholderCell(indexPath) {
            placeholderText = __("agenda.noActions.text")
        }
        else if indexPathIsDecisionTextPlaceholderCell(indexPath) {
            placeholderText = __("agenda.noDecisions.text")
        }
        textCell.setup(placeholderText)
    }

    return cell
}

public func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
    if (section == 0) {
        return nil
    }
    let subSection = sectionHelper.subSectionForSection(section)
    let cellId = isProtocolMode && subSection == 0 ? protocolHeaderCellId : headerCellId
    let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier(cellId)
    let agenda = agendaForSection(section)

    if let standardHeaderCell = cell as? StandardTableViewHeaderCell {
        let subSection = sectionHelper.subSectionForSection(section)

        let currentSectionIsEmpty = sectionIsEmpty(section)
        let protocolIsLocked = selectedMeeting!.protocolIsLocked
        if (isPreviousProtocolMode && currentSectionIsEmpty) {
            return nil
        }
        let allowEditing = (isInEditProtocolMode || !isProtocolMode) && !isPreviousProtocolMode && !protocolIsLocked
        let showRightAddButton = ((subSection == 1 && currentSectionIsEmpty) || subSection == 2 || (subSection == 3 && indexPathShowingPlaceholderNewDecisionCell?.section != section)) && allowEditing

        let headerTitle = headerTitleList[subSection]
        standardHeaderCell.setupWithText(headerTitle, section:section, showAddButton: showRightAddButton, delegate: self)
    }
    else if let protocolHeaderCell = cell as? ProtocolTableViewHeaderCell {
        let showSeparator = section > 1
        let onlyShowAttachmentIfItHaveAttachments = isPreviousProtocolMode || (isProtocolMode && !isInEditProtocolMode)
        let showAttachmentIcon = !onlyShowAttachmentIfItHaveAttachments || agenda.attachments.count > 0
        protocolHeaderCell.setup(showSeparator: showSeparator, agenda: agenda, section: section, showProtocolIcon: false, showAttachmentIcon: showAttachmentIcon, delegate: self)
    }

    return cell!.wrappedInNewView()
}

// In UIView extension
func wrappedInNewView() -> UIView
{
    let view = UIView(frame: frame)
    autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
    view.addSubview(self)
    return view
}
Community
  • 1
  • 1
Sunkas
  • 9,542
  • 6
  • 62
  • 102
  • 3
    I don't believe the exception is in any of the code you show. – trojanfoe Apr 08 '16 at 13:02
  • can you provide more detail about your insertion and function which is playing with data – Tarun Seera Apr 08 '16 at 13:40
  • Added more from the datasource of the tableView. I do not do any insertions, only reloading the tableview to update the height of a cell when entering text into it. It crashes when the height changes and the tableview is reloaded (in `endUpdates()`) – Sunkas Apr 08 '16 at 14:02
  • Is it advisable to call `beginUpdates()` and `endUpdates()` with nothing between? – trojanfoe Apr 08 '16 at 14:03
  • From all the tutorials for iOS 8 self-sizing cell with `UITextView` dynamic height changes this seems to be the only way to keep focus in the `UITextView` when adjusting cell heights. – Sunkas Apr 08 '16 at 14:15
  • This for example: http://candycode.io/self-sizing-uitextview-in-a-uitableview-using-auto-layout-like-reminders-app/ – Sunkas Apr 08 '16 at 14:17
  • @Sunkas what should be the use of the example link ? does we have to solve problem from that? or you have taken your code but your code is not working ? – HardikDG Apr 10 '16 at 17:35
  • @Pyro: It was response to @trojanfoe and a reference to to that it is advisable to call `begingUpdates()` and `endUpdatesUI`. – Sunkas Apr 10 '16 at 18:43
  • `UIView.setAnimationsEnabled(false)` won't have any effect there because `UITableView` uses Core Animations, so you would have to call `CATransaction.setDisableActions(true)` between `beginUpdates()` and `endUpdates`. Also, changing `contentOffset` directly is not probably advisable, I would use `setContentOffset(_:animated:)` – Sulthan Apr 10 '16 at 18:48
  • 1
    @trojanfoe Calling `beginUpdates(); endUpdates()` with anything in between is a documented (in `UITableView.beginUpdates()` docs) way to update heights of cells. – Sulthan Apr 10 '16 at 18:52
  • Can you check (through logging or breakpoints) if it's possible that your `reloadDataAnimatedKeepingOffset` method is being called multiple times in a row (ie. before the previous animation finishes?)? Also, when it doesn't crash, does the tableview height change animate? – Jack Apr 10 '16 at 18:57
  • Also, since the other question you linked to points to `estimate` as the culprit, I noticed in `estimatedHeightForHeaderInSection` you return `0` for the first section. A `0` here actually means "no estimate` according to the docs and not "0 height". Since in the actual `heightForHeader` method you return 0, try returning anything other than 0 in the estimate method. – Jack Apr 10 '16 at 19:05
  • @Sulthan, Jack Wu. Thanks for ideas but does not help, see **Edit3** in question. – Sunkas Apr 11 '16 at 07:10
  • @JackWu: Yes, when not crashing it works as intended, it animated. – Sunkas Apr 11 '16 at 07:11
  • 4
    Could you create a minimal compilable example and put it on github? – Sulthan Apr 11 '16 at 07:37
  • Please show your implementations of `func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell` and `func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int` since this seems to be related to the data source. – user212514 Apr 14 '16 at 04:06
  • @Sultan I will get back to you on that one. Started a example project with varying cell height and editable textviews. However, no crashes. So my guess is it's related to the headers or conflicting reloads. – Sunkas Apr 14 '16 at 06:43
  • @user212514 I've added these methods to the question. – Sunkas Apr 14 '16 at 06:43
  • Where do you call `updateTextViewHeight` from and when? – Johannes Fahrenkrug Apr 15 '16 at 12:55
  • I wonder if you had any success with this? I've got the same issue. The suggested workaround of using a fixed estimate (property instead of delegate method) haven't helped. In my case it is also a dynamically sized cell holding a `UITextView` (that grows instead of scrolling). I _think_ that this has only started to happen since turning on automatic cell sizing. So I might try putting computed estimates back in for this kind of cell. – Benjohn Apr 11 '17 at 15:21
  • Yeah. Turning off automatic cell (and title) sizing seems to prevent the crash – it happened easily before, I've not seen it happen now. That's not much of a work around though! … I guess perhaps I could put all my explicitly computed sizing code back in :-/ – Benjohn Apr 11 '17 at 15:36
  • If you want to _not_ use automatic cell sizing, it is still possible to manually perform the calculations as [described in detail in this answer](http://stackoverflow.com/a/18746930/2547229). – Benjohn Apr 12 '17 at 12:19
  • It feels like a step backwards to skip self-sizing cells. The solution for us was to not use edit text in `UITextView` in a `UITableViewCell`. We just open up a new ViewController where you can edit the text instead. – Sunkas Apr 12 '17 at 12:27

1 Answers1

4

Here is radar about this iOS bug: http://openradar.appspot.com/15729686 All what I can suggest you to do is to replace this:

public func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 48.0
}

with this:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 48.0

If still crashes, then also try to remove tableView:estimatedHeightForHeaderInSection: method

We should cross our fingers and hope this radar will be closed with new iOS X :)

k06a
  • 17,755
  • 10
  • 70
  • 110