33

Today I started to get my apps ready for iOS8. I discovered that the subtitles of my UITableCells won't update within viewWillAppear. I brought that down to a minimal example:

I've got a static cell TableViewController with 2 cells (style = subtitle) One subtitle is empty the other one is set.

Interface builder screenschot

I update the subtitles like this:

- (void) viewWillAppear:(BOOL)animated
{
  [super viewWillAppear:animated];
  [[self.without detailTextLabel] setText:@"foobar"];
  [[self.with detailTextLabel] setText:@"barfoo"];
}

While everythin works under iOS7 (and 6 and 5), iOS8 won't update the title of the first cell.

screenshot of simulator

However, when I touch the cell it will update and show the text.

Is this a simulator issue? A bug? Am I doing something wrong?

lupz
  • 3,620
  • 2
  • 27
  • 43
  • have you tried putting that code in the viewdidload method instead? – A O Sep 11 '14 at 17:34
  • Although I need it to happen within `viewWillAppear` I tried it out. `viewDidLoad` shows exact the same (iOS8 no title, – lupz Sep 12 '14 at 08:28
  • did you initialize your without object like this? without = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; – A O Sep 12 '14 at 12:26
  • No. It is an outlet from interface builder. I guess it is initialized automatically when the storyboard is loaded. Today I updated an iPod to iOS8 to test on a real device. I used the app that is already in the store (build with iOS7 SDK). Again everything is fine with iOS7 and below. Strangely, the subtitle will shown up on iOS8 but gets cut off (like "foo..." instead of foobar). So I think this may be related to updating the subtitles view size. – lupz Sep 12 '14 at 13:43
  • I found a hint among the [iOS8 prerelease release notes](https://developer.apple.com/library/prerelease/ios/releasenotes/General/RN-iOSSDK-8.0/index.html) stating _UILabel has a default value of YES for clipsToBounds. This differs from the normal UIView default of NO._ But changing that flag does not change anything. – lupz Sep 12 '14 at 14:20
  • Problem not solved with iOS 8.0.2. Can someone tell if iOS 8.1 beta 2 solves this bug? – Imanou Petit Oct 08 '14 at 14:41

6 Answers6

48

As a temporary work around I simple introduced a blank space in the text label and programmatically where I set the label text to nil, I set it to a blank space

cell.detailTextLabel.text = @" ";

Adjust your logic accordingly to deal with a lone blank space as nil.

For me the detail text label didn't show up even though I had a string ready for it before the viewcontroller loaded. one of my many cells shows the current date in a detail text label as default whenever the view controller appears, but that didn't work either.

I think this has something to do with the fact that the iOS 8 is not able to update the text label's text if it set to nil initially.

So see to it that you introduce a blank space in your prototype cell as well, in interface builder. Surprisingly text in labels of my custom cells are working fine.

AceN
  • 771
  • 10
  • 18
  • I tested this in a couple of scenes and it seems to work quite well. The actual value ([1..3] users choice) is kept in a model object. The text label is a localized string that will be loaded if such a value is set. So there is no code that relies on the label's text or state and I don't need to adjust any logic. – lupz Sep 29 '14 at 13:55
  • 1
    Yes, this works. If there is always non-empty text in the cell, then Apple's code never removes the label from the view hierarchy, and layouts always work as expected. This is not a problem for custom cells because in that case Apple is not managing the subviews; they are always in the view hierarchy (or it is your own code removing them). It is purely a problem for the cell styles which make use of the detailTextLabel, as the timing is wrong when it gets re-added to the view hierarchy after non-empty text is set to it. – Carl Lindberg Sep 29 '14 at 19:31
  • My alternate fix above though should mean you don't need to change any other code (or change it back when Apple fixes the bug). (@lupz -- does the second version work for you?) I normally do not like swizzling Apple's methods, but fixing Apple bugs seems to be one of the better uses, as once Apple fixes the bug the swizzle can be limited to just the problem iOS versions and eventually removed altogether, without affecting the rest of the code base either way. – Carl Lindberg Sep 29 '14 at 19:34
  • @CarlLindberg Thank you. The updated app is shipped and I had to move on to other projects. I'll test your approach when I have the time but it actually looks like an endless recursive loop (`_detailfix_layoutSubviews` calling itself). – lupz Oct 01 '14 at 12:23
  • 1
    actually no, with method_exchangeImplementations what looks like a recursive call is actually calling the original implementation. The body of that method is installed as the real layoutSubviews, and the original implementation is switched over the the _detailfix_layoutSubviews method. – Carl Lindberg Oct 01 '14 at 20:56
  • @CarlLindberg i dont thing it is removing the view from the hierarchy. Cause with the labels set to nil when i try and set the labels programatically, it is resizing my text label above the detail text label, by shifting it upwards, as i would expect it to when showing text in the detail text label. They text also appears automagically when i select the cell! I think it might have something to do with auto layout not initially stretching the label in width to fit the Cell's width. Will try tomorrow by coloring the label to see how it initially looks when its text is nil. – AceN Oct 04 '14 at 22:24
  • @CarlLindberg Ah yeah. I guess I see your point, sry. Thanks for that drive-by introduction to swizzling :) – lupz Oct 06 '14 at 08:38
  • i have filed a bug report and apple engineering has requested me a demo project to show whats happening. I have emailed them the same. apparently its also a problem with the text label and not just the detail text label. – AceN Oct 07 '14 at 02:14
  • 3
    @AceNeerav The detailTextLabel is definitely getting removed from the view hierarchy. In a situation where it is not working, try looking at cell.detailTextLabel.superview. It gets re-added to the view hierarchy during layoutSubviews, but it remains at size zero (its size with zero text) at first because auto layout has not resized it. During the *next* layout pass (caused say by selecting a cell or rotating), then it gets sized correctly and shows up. My radar (18344249) has a demo app. I have not seen an issue with textLabel but it's always possible. – Carl Lindberg Oct 07 '14 at 21:51
  • @CarlLindberg i didnt quite understand. When is it getting removed and when is it being readded? And yes i checked in my demo app it doesnt work for programmatically set text labels either unless they have atleast one character. – AceN Oct 09 '14 at 02:24
  • There is a private layout manager for table view cells, which switches up based on cell style, and gets called during layoutSubviews. It looks like that will remove the detail label if there is empty text, and re-add it if there is some. Unfortunately, it gets added only after auto layout has done its thing, so it remains at size zero initially, until the next display pass. The regular textLabel seems to get added as a subview when you call the *accessor* method, so it's not being handled by the layout manager I didn't think, but it's possible there are similar issues with it. – Carl Lindberg Oct 10 '14 at 17:49
  • I am having this problem tonight - was there never a resolution to this? – SAHM Feb 09 '15 at 02:12
  • 1
    @AceNeerav Thank you for the workaround. Although I didn't have to set the empty string in both code and IB. Setting it from just the IB worked for me. – Isuru May 14 '15 at 08:53
  • 1
    oh, finally...really stupid behavior of detailText :) – Konstantin Jul 17 '15 at 07:38
15

It appears Apple added an optimization which removes the detailTextLabel from the view hierarchy when the value is nil, and re-adds as necessary. Unfortunately in my tests, it gets added during a layout pass, but only after it would have been resized to fit the new text by that layout pass, so the size is still zero. The next layout pass (say on a rotation) then it will display OK.

One workaround may be to forcibly add the label back into the view hierarchy in all circumstances (seems like it should be OK if zero-sized). I'd be interested to see if dropping the below category into your app fixes things.

#import <objc/runtime.h>
/*
 * In iOS8 GM (and beta5, unsure about earlier), detail-type UITableViewCells (the
 * ones which have a detailTextLabel) will remove that label from the view hierarchy if the
 * text value is nil.  It will get re-added during the cell's layoutSubviews method, but...
 * this happens just too late for the label itself to get laid out, and its size remains
 * zero.  When a subsequent layout call happens (e.g. a rotate, or scrolling the cells offscreen
 * and back) then things get fixed back up.  However the initial display has completely blank
 * values.  To fix this, we forcibly re-add it as a subview in both awakeFromNib and prepareForReuse.
 * Both places are necessary; one if the xib/storyboard has a nil value to begin with, and
 * the other if code explicitly sets it to nil during one layout cycle then sets it back).
 * Bug filed with Apple; Radar 18344249 .
 *
 * This worked fine in iOS7.
 */
@implementation UITableViewCell (IOS8DetailCellFix)

+ (void)load
{
    if ([UIDevice currentDevice].systemVersion.intValue >= 8)
    {
        /* Swizzle the prepareForReuse method */
        Method original = class_getInstanceMethod(self, @selector(prepareForReuse));
        Method replace  = class_getInstanceMethod(self, @selector(_detailfix_prepareForReuse));
        method_exchangeImplementations(original, replace);

        /*
         * Insert an awakeFromNib implementation which calls super.  If that fails, then
         * UITableViewCell already has an implementation, and we need to swizzle it instead.
         * In IOS8 GM UITableViewCell does not implement the method, but they could add one
         * in later releases so be defensive.
         */
        Method fixawake = class_getInstanceMethod(self, @selector(_detailfix_super_awakeFromNib));
        if (!class_addMethod(self, @selector(awakeFromNib), method_getImplementation(fixawake), method_getTypeEncoding(fixawake)))
        {
            original = class_getInstanceMethod(self, @selector(_detailfix_awakeFromNib));
            replace = class_getInstanceMethod(self, @selector(awakeFromNib));
            method_exchangeImplementations(original, replace);
        }
    }
}

- (void)__detailfix_addDetailAsSubviewIfNeeded
{
    /*
     * UITableViewCell seems to return nil if the cell style does not have a detail.
     * If it returns non-nil, force add it as a contentView subview so that it gets
     * view layout processing at the right times.
     */
    UILabel *detailLabel = self.detailTextLabel;
    if (detailLabel != nil && detailLabel.superview == nil)
    {
        [self.contentView addSubview:detailLabel];
    }
}

- (void)_detailfix_super_awakeFromNib
{
    [super awakeFromNib];
    [self __detailfix_addDetailAsSubviewIfNeeded];
}

- (void)_detailfix_awakeFromNib
{
    [self _detailfix_awakeFromNib];
    [self __detailfix_addDetailAsSubviewIfNeeded];
}

- (void)_detailfix_prepareForReuse
{
    [self _detailfix_prepareForReuse];
    [self __detailfix_addDetailAsSubviewIfNeeded];
}

@end

There might be other approaches -- if you can call setNeedsLayout at the right time, it may force an additional layout pass which corrects things, but I was not able to find the right time for that.

EDIT: A comment below indicated that re-displayed cells could be an issue. So, a simpler fix may be to just swizzle layoutSubviews and do the check before calling Apple's implementation. That could solve all issues since it is during the layout call that the problem happens. So, below is that version of the fix -- I would be interested to see if that works.

#import <objc/runtime.h>

@implementation UITableViewCell (IOS8DetailCellFix)

+ (void)load
{
    if ([UIDevice currentDevice].systemVersion.intValue >= 8)
    {
        Method original = class_getInstanceMethod(self, @selector(layoutSubviews));
        Method replace  = class_getInstanceMethod(self, @selector(_detailfix_layoutSubviews));
        method_exchangeImplementations(original, replace);
    }
}

- (void)_detailfix_layoutSubviews
{
    /*
     * UITableViewCell seems to return nil if the cell type does not have a detail.
     * If it returns non-nil, force add it as a contentView subview so that it gets
     * view layout processing at the right times.
     */
    UILabel *detailLabel = self.detailTextLabel;
    if (detailLabel != nil && detailLabel.superview == nil)
    {
        [self.contentView addSubview:detailLabel];
    }

    [self _detailfix_layoutSubviews];
}

@end

EDIT: It appears this bug is fixed in iOS9. So, the condition can be changed to:

if ([UIDevice currentDevice].systemVersion.intValue == 8)

If an application only needs to support iOS9 and above, the swizzle fix category can just be removed.

Carl Lindberg
  • 2,902
  • 18
  • 22
  • Excellent, I had this issue and this fixed it for me. Going to dupe the radar. – Mike Mertsock Sep 21 '14 at 13:21
  • Thank you. While this fixes the example issue perfectly it only fixes the half of the original. I present another viewcontroller to let the user select the value. When the subview is dismissed iOS calls `viewWillAppear` on the parent. At this point, the tablecells are already created and the swizzle code won't be invoked - it still refuses to update the subtitle. I'll stick with calling `setNeedsLayout` from `viewDidAppear`. – lupz Sep 22 '14 at 09:29
  • Interesting. If you called reloadData on viewWillAppear, presumably that would do it though. But, maybe it would be better to swizzle layoutSubviews on UITableViewCell instead (and do the processing before calling the original). I may see if that works; might be a simpler patch. – Carl Lindberg Sep 22 '14 at 15:09
  • The second version seems to work fine for me. I originally noticed the issue when reusing cells, and this takes care of my test cases. – GSnyder Sep 30 '14 at 06:31
  • Thank you. Any idea if Apple are doing something to solve this problem ? – Petar May 29 '15 at 12:45
  • It looks like Apple fixed this in iOS9; I edited the answer to note that. – Carl Lindberg Oct 06 '15 at 22:52
  • Broken again in tvOS 10. :-( – drekka Nov 24 '16 at 04:18
4

A few days passed and I think I'll have to use a workaround:

- (void) viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.without setNeedsLayout];
}

This has a visible delay but it's the only way I could find to make the text appear and be readable on iOS8.

lupz
  • 3,620
  • 2
  • 27
  • 43
  • 2
    Did not work for me, but adding `[self.tableView reloadData]` to `viewDidAppear:` fixed with same result; momentary visible delay but the cells eventually look right. – Thompson Oct 08 '14 at 18:55
  • @Thompson That did it for me as well. – Bart van Kuik Nov 14 '14 at 13:49
  • 1
    @lupz in my case, `setNeedsLayout` didn't work but `[self.without layoutSubviews]` did. – 1in9ui5t Dec 10 '14 at 18:29
  • This was chosen as the right answer only because it APPEARS to fix the problem, but I assure you it does not. It merely hides the problem part of the time. It is caused by a bug in an optimization in iOS8 which removes the detailTextLabel from the view hierarchy. See @AceNeerav's answer below. – Kenny Wyland Mar 09 '15 at 22:15
  • Actually, check out @Carl Lindberg's solution instead. Using @" " in detailTextLabel causes the table cells to be laid out as if there is a detail label, even though there really shouldn't be. Carl's solution is transparent. – Kenny Wyland Mar 10 '15 at 02:06
3

Confirmed. iOS8 is not drawing detailText if it's initialized to nil. This simple line should do the trick.

self.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:@" "];
4m1r
  • 12,234
  • 9
  • 46
  • 58
1

I have the same problem, in the cellForRowAtIndexPath I call cell.layoutSubviews() before return cell. It solves the problem.

JZAU
  • 3,550
  • 31
  • 37
1

I have seen two causes of this issue on iOS8.

  1. If you set the text in the detail label to nil or "" (most frequent places where this is done is in the prepareForReuse and/or deleting the default text from the storyboard) [As indicated in the answers above, this issue is resolved in iOS9]

  2. If you subclass a UITableViewCell of type subtitle and then register the cell which you created in the storyboard programmatically. [This is a programming error, you do not need to register prototype cells created in storyboard]

runios
  • 384
  • 1
  • 2
  • 14