Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil' when performing UITableView animated reload

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
}
like image 386
Sunkas Avatar asked Apr 07 '16 07:04

Sunkas


1 Answers

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 :)

like image 144
k06a Avatar answered Oct 18 '22 23:10

k06a