Почему автоматическое расположение в ячейках табличного представления приводит к неправильному изменению размера содержимого таблицы?


Я использую Xcode 4.6.1, и у меня есть приложение iOS 6.1.

У него есть табличное представление, которое детализируется в другое табличное представление. Во втором табличном представлении для отображения данных используются пользовательские ячейки. Однако существует проблема со вторым табличным представлением: его размер содержимого обычно неверен (в частности, высота, которая либо слишком высока, либо слишком коротка). Это либо приводит к тому, что в нижней части есть дополнительное пространство, либо недостаточно места для прокрутки вниз.

В на рисунке ниже показаны четыре скриншота из тестового приложения, которое я сделал и которое воспроизводит проблему. Я собираюсь пройтись по ступеням, ведущим к моей проблеме.

Четыре изображения из моего тестового приложения

  1. Приложение загружается, и отображается первая таблица. Он имеет три строки, которые сверлятся во вторую таблицу.

  2. Я выбираю первую строку, которая называется "Список: от 0 до 10". Навигационный контроллер выдвигает на экран вторую таблицу. Он имеет 11 строк, и (правильно) имеет содержание размер для 11 рядов.

  3. Затем я вернулся и выбрал следующую строку, которая называется "List: A to P". Вторая таблица снова появляется на экране, и теперь в ней 16 строк. Однако он имеет только размер содержимого для 11 строк. Остальные 5 строк все еще внизу, но размер содержимого продолжает отбрасывать меня назад к 11-му ряду.

  4. Затем я вернулся назад и выбрал последнюю строку, которая называется "List: a to f". Вторая таблица снова появляется на экране, и в ней есть 6 строк. Однако, он по-прежнему имеет размер содержимого для 11 строк. В нижней части таблицы слишком много места, и она показывает 5 ячеек внизу, в которых ничего нет.

Шаблон здесь заключается в том, что размер содержимого остается на том же уровне, что и при первоначальной загрузке. Таким образом, первая строка, которую я выберу, будет иметь правильный размер содержимого, но все остальные теперь останутся на этом размере, независимо от того, сколько строк у них есть. Это также означает, что если бы я сначала выбрал "список: от a до f", который имеет 6 строк, тогда все остальные будут иметь размер содержимого для 6 строк, потому что он остается на любом размере содержимого для первой строки , которую я выбрал.

В моем коде я никогда не изменяю размер содержимого сам; все это происходит автоматически. Чтобы выяснить, когда и почему представление таблицы изменяло свой размер содержимого, я установил наблюдателя на свойство "contentSize" второй таблицы. После того, как я сделал это, я понял, что размер контента не остается прежним, но что это было на самом деле изменение дважды при выборе строки: сначала до нужного размера (при перезагрузке табличного представления), а затем возврат к старому размеру (при появлении табличного представления).

Поэтому я пытаюсь понять, почему он возвращается к старому размеру. Чтобы увидеть, что происходит за кулисами, я заставил его распечатывать трассировку стека каждый раз, когда он наблюдал изменение размера содержимого.

Ниже вы можете увидеть журнал консоли для предыдущих шагов, через которые вы прошли. Однако, чтобы сэкономить место и сделать его более читаемым, Я заменил длинные трассировки стека сообщением типа "(Stack Trace Type A)". Я покажу вам их детали позже, но сейчас я просто называю их трассировкой стека типа A, B или C.

first table: selected row 0 (List: 0 to 10)
second table: viewDidLoad
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type A)
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type B)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear

first table: selected row 1 (List: A to P)
OBSERVED CHANGE: contentSize.height = 704 (row count = 16)
(Stack Trace Type A)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 16)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear

first table: selected row 2 (List: a to f)
OBSERVED CHANGE: contentSize.height = 264 (row count = 6)
(Stack Trace Type A)
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 6)
(Stack Trace Type C)
second table: viewDidAppear
*going back to first table*
second table: viewWillDisappear
second table: viewDidDisappear

Как вы можете видеть, он возвращает размер содержимого обратно к старому размеру. В этом случае это будет 484, что достаточно для отображения 11 ячеек с высотой 44. Если все работало правильно, наблюдаемое изменение, которое связано с трассировка стека типа C не будет происходить. Другими словами, тип С-это то, что вызывает проблему.

Теперь я покажу вам, как на самом деле выглядят следы стека. Обратите внимание, что первые два (А и в) не связаны с проблемой. Я показываю их только для того, чтобы вы могли сравнить/сравнить их с типом C, который, по-видимому, является причиной проблемы.

(Тип Трассировки Стека A) Когда табличное представление изменило свой размер содержимого из-за перезагрузки, трассировка стека выглядеть так.

(
    0   CustomTableCellTest   0x00002fe0 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
    1   Foundation            0x00b0d417 NSKeyValueNotifyObserver + 357
    2   Foundation            0x00b26b24 NSKeyValueDidChange + 456
    3   Foundation            0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
    4   Foundation            0x00b48b80 _NSSetSizeValueAndNotify + 185
    5   UIKit                 0x000b57ef -[UITableView(_UITableViewPrivate) _updateContentSize] + 782
    6   UIKit                 0x000c3974 -[UITableView noteNumberOfRowsChanged] + 154
    7   UIKit                 0x000c32dc -[UITableView reloadData] + 769
    8   CustomTableCellTest   0x00004d97 -[MetaMasterViewController tableView:didSelectRowAtIndexPath:] + 567
    9   UIKit                 0x000c7285 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1194
    10  UIKit                 0x000c74ed -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
    11  Foundation            0x00ad15b3 __NSFireDelayedPerform + 380
    12  CoreFoundation        0x01c55376 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
    13  CoreFoundation        0x01c54e06 __CFRunLoopDoTimer + 534
    14  CoreFoundation        0x01c3ca82 __CFRunLoopRun + 1810
    15  CoreFoundation        0x01c3bf44 CFRunLoopRunSpecific + 276
    16  CoreFoundation        0x01c3be1b CFRunLoopRunInMode + 123
    17  GraphicsServices      0x01bf07e3 GSEventRunModal + 88
    18  GraphicsServices      0x01bf0668 GSEventRun + 104
    19  UIKit                 0x00017ffc UIApplicationMain + 1211
    20  CustomTableCellTest   0x000021ed main + 141
    21  CustomTableCellTest   0x00002115 start + 53
)

(Тип Трассировки Стека B) Это происходит только в первый раз, когда появляется табличное представление, поэтому это происходит только один раз. Я считаю, что это происходит, когда таблица изначально загружает свое представление.

(
    0   CustomTableCellTest   0x00002fe0 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
    1   Foundation            0x00b0d417 NSKeyValueNotifyObserver + 357
    2   Foundation            0x00b26b24 NSKeyValueDidChange + 456
    3   Foundation            0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
    4   Foundation            0x00b48b80 _NSSetSizeValueAndNotify + 185
    5   UIKit                 0x000b57ef -[UITableView(_UITableViewPrivate) _updateContentSize] + 782
    6   UIKit                 0x000cbc8e -[UITableView _rectChangedWithNewSize:oldSize:] + 261
    7   UIKit                 0x000cc231 -[UITableView setFrame:] + 279
    8   UIKit                 0x000f5014 +[UIViewControllerWrapperView wrapperViewForView:frame:] + 448
    9   UIKit                 0x00110e18 -[UINavigationController _startTransition:fromViewController:toViewController:] + 239
    10  UIKit                 0x0011189b -[UINavigationController _startDeferredTransitionIfNeeded:] + 386
    11  UIKit                 0x00111e93 -[UINavigationController pushViewController:transition:forceImmediate:] + 1030
    12  UIKit                 0x00111a88 -[UINavigationController pushViewController:animated:] + 62
    13  CustomTableCellTest   0x00004e21 -[MetaMasterViewController tableView:didSelectRowAtIndexPath:] + 705
    14  UIKit                 0x000c7285 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1194
    15  UIKit                 0x000c74ed -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 201
    16  Foundation            0x00ad15b3 __NSFireDelayedPerform + 380
    17  CoreFoundation        0x01c55376 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
    18  CoreFoundation        0x01c54e06 __CFRunLoopDoTimer + 534
    19  CoreFoundation        0x01c3ca82 __CFRunLoopRun + 1810
    20  CoreFoundation        0x01c3bf44 CFRunLoopRunSpecific + 276
    21  CoreFoundation        0x01c3be1b CFRunLoopRunInMode + 123
    22  GraphicsServices      0x01bf07e3 GSEventRunModal + 88
    23  GraphicsServices      0x01bf0668 GSEventRun + 104
    24  UIKit                 0x00017ffc UIApplicationMain + 1211
    25  CustomTableCellTest   0x000021ed main + 141
    26  CustomTableCellTest   0x00002115 start + 53
)

Таким образом, Тип A и B возникают, когда все работает правильно. Однако, когда вещи не работают правильно, я также получаю эту следующую трассировку стека.

(Тип Трассировки Стека C) Это трассировка стека, которая происходит, когда содержимое размер возвращается к старому размеру.

(
    0   CustomTableCellTest   0x00003080 -[MasterViewController observeValueForKeyPath:ofObject:change:context:] + 320
    1   Foundation            0x00b0d417 NSKeyValueNotifyObserver + 357
    2   Foundation            0x00b26b24 NSKeyValueDidChange + 456
    3   Foundation            0x00adbd60 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 131
    4   Foundation            0x00b48b80 _NSSetSizeValueAndNotify + 185
    5   UIKit                 0x0007c213 -[UIScrollView _resizeWithOldSuperviewSize:] + 161
    6   UIKit                 0x0005cf2a -[UIView(Geometry) resizeWithOldSuperviewSize:] + 72
    7   UIKit                 0x0005bb28 __46-[UIView(Geometry) resizeSubviewsWithOldSize:]_block_invoke_0 + 80
    8   CoreFoundation        0x01cb85a7 __NSArrayChunkIterate + 359
    9   CoreFoundation        0x01c9003f __NSArrayEnumerate + 1023
    10  CoreFoundation        0x01c8fa16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
    11  UIKit                 0x0005babf -[UIView(Geometry) resizeSubviewsWithOldSize:] + 149
    12  UIKit                 0x00557dcc -[UIView(AdditionalLayoutSupport) _is_layout] + 143
    13  UIKit                 0x000607ae -[UIView(Hierarchy) layoutSubviews] + 80
    14  UIKit                 0x000682dd -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 279
    15  libobjc.A.dylib       0x010e76b0 -[NSObject performSelector:withObject:] + 70
    16  QuartzCore            0x02292fc0 -[CALayer layoutSublayers] + 240
    17  QuartzCore            0x0228733c _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 468
    18  QuartzCore            0x02287150 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 26
    19  QuartzCore            0x022050bc _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 324
    20  QuartzCore            0x02206227 _ZN2CA11Transaction6commitEv + 395
    21  QuartzCore            0x022068e2 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 96
    22  CoreFoundation        0x01c5eafe __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 30
    23  CoreFoundation        0x01c5ea3d __CFRunLoopDoObservers + 381
    24  CoreFoundation        0x01c3c7c2 __CFRunLoopRun + 1106
    25  CoreFoundation        0x01c3bf44 CFRunLoopRunSpecific + 276
    26  CoreFoundation        0x01c3be1b CFRunLoopRunInMode + 123
    27  GraphicsServices      0x01bf07e3 GSEventRunModal + 88
    28  GraphicsServices      0x01bf0668 GSEventRun + 104
    29  UIKit                 0x00017ffc UIApplicationMain + 1211
    30  CustomTableCellTest   0x000021ed main + 141
    31  CustomTableCellTest   0x00002115 start + 53
)

Как вы можете видеть, есть некоторые QuartzCore вещи, происходящие, которые не присутствовали в типе A или B. Кроме того, на нескольких строках в типе C, на самом деле есть методы, вызываемые за кулисами со словами "OldSize" в их названиях. Так что, по-видимому, этодействительно возвращает ему "старый размер". Однако я до сих пор не понимаю, почему эти методы QuartzCore и "OldSize" называются в первую очередь.

Я пытался поиск "resizeSubviewsWithOldSize", но единственная документация , которую я смог найти на нем, была для NSView (которая предназначена для приложений OS X, а не iOS, которые используют UIView). Это описание в документах на самом деле не помогло мне понять , Почему он был вызван в этом случае.

Тем не менее, хотя я и не знаю, почему это называется, У меня есть довольно хорошее представление о , Когда это называется. С большим количеством тестов я понял, что он возвращается к старому размеру контента когда появляется табличное представление, а иногда и когда оно исчезает. Например, когда я сделал модальный вид, закрывающий экран, а затем уходящий, размер содержимого был фактически изменен 3 раза. Сообщения в журнале выглядели примерно так.
*Modal view entering*
second table: viewWillDisappear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidDisappear

*Modal view exiting*
second table: viewWillAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
second table: viewDidAppear
OBSERVED CHANGE: contentSize.height = 484 (row count = 11)
(Stack Trace Type C)
Таким образом, изменение не связано с перезагрузкой табличного представления: оно связано с его появлением и исчезновением. Я предполагаю, что представления пытаются компоновать свои подвиды каждый раз, когда они появляются, поэтому, похоже, есть проблема с тем, как мой пользователь клетки разложены по местам. Первоначально я думал, что проблема была в том, как мои пользовательские ячейки были настроены: либо в моем коде, либо в xib. Это было потому, что проблема исчезла, когда я не использовал свои собственные ячейки. Но однажды я выключил автоматическую раскладку в своей пользовательской ячейке xib, и проблема также исчезла. Так что, по-видимому, проблема имеет какое-то отношение к автоматической компоновке.

Для удобства отладки способ, которым я настраиваю ячейку в xib, очень прост: единственная вещь на ячейка представляет собой UILabel с серым фоновым цветом. Все ограничения автоматической компоновки были установлены конструктором интерфейсов по умолчанию. Ниже вы можете увидеть, как выглядит моя пользовательская ячейка xib.

MasterCell xib

Я также попытался удалить UILabel, чтобы на ячейке ничего не было, и я оставил автоматический макет включенным. Когда я сделал это, проблема тоже исчезла. Так что я думаю, что-то не так с тем, как UILabel установлен на ячейке. Что странно, так как он был создан с этими " фиолетовыми" ограничения по умолчанию от Interface Builder, которые я не могу удалить.

После того, как я огляделся в поисках информации об автоматическом макете в ячейках табличного представления, я наткнулся на этот вопрос. Очевидно, конструктор интерфейсов неправильно устанавливает ограничения для представления содержимого ячейки. Я не был уверен, связано ли это с моей проблемой, но все равно попробовал. Я использовал решение, опубликованное Адрианом, которое выглядело так.

//  MasterCell.m
//  My custom UITableViewCell subclass

- (void)awakeFromNib {
    [super awakeFromNib];

    for (NSLayoutConstraint *cellConstraint in self.constraints) {

        [self removeConstraint:cellConstraint];

        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;

        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                     attribute:cellConstraint.firstAttribute
                                     relatedBy:cellConstraint.relation
                                        toItem:seccondItem
                                     attribute:cellConstraint.secondAttribute
                                    multiplier:cellConstraint.multiplier
                                      constant:cellConstraint.constant];

        [self.contentView addConstraint:contentViewConstraint];
    }
}

Однако, пока он, казалось, делал то, что было задумано сделать (то есть изменить ограничения, чтобы они были связаны с представлением содержимого, а не с самой ячейкой), это все еще не решило мою проблему: размер содержимого все еще возвращался к старому размеру.

Итак, в заключение, и чтобы прояснить этот довольно длинный вопрос, это в основном то, что я спрашиваю.

Почему, когда я включаю автоматическую разметку в моей пользовательской ячейке таблицы xib, размер содержимого табличного представления неправильно возвращается к "старому размеру", когда таблица появляется на экране?

1 4

1 ответ:

Вы повторно используете тот же экземпляр контроллера табличного представления для "главной" таблицы, но отправляете ему сообщения в неправильном порядке.

Вы нажимаете на контроллер следующим образом:

if (!self.masterViewController) {
    self.masterViewController = [[MasterViewController alloc] init];
}

self.masterViewController.objects = self.masterObjects[indexPath.row];

[self.masterViewController.tableView reloadData];
[self.navigationController pushViewController:self.masterViewController animated:YES];

Если вы просто поменяете местами reloadData и push-линии, то ваша проблема исчезнет.

Я не совсем уверен, что здесь происходит, но я думаю, что проблема в том, что после того, как главный контроллер выскочил, у него нет никакой системы отсчета (каламбур намеренно!) чтобы вычислить соответствующий размер контента при его перезагрузке, поэтому он, вероятно, не кэширует размер контента для этой новой высоты.

Затем, когда представление выталкивается, вид прокрутки внезапно снова имеет контекст, он знает, что пересчитал и поэтому использует размер кэшированного содержимого. Во всяком случае, такое впечатление я получаю (как и вы) от трассировки стека. Первый стол, который вы выбираете, всегда подходит, так как нет старого размера. Если бы вы сделали это с помощью раскадровок, вы были бы каждый раз создавайте новый мастер-контроллер, и вы никогда не заметили бы проблемы.

Что касается того, почему autolayout имеет какое-либо значение, ну, опять же, я только предполагаю и размахиваю руками здесь, но при autolayout все происходит намного позже в жизненном цикле контроллера вида, чем при явном макете. Если у вас есть autolayout полностью вниз, то в представлении без superview (и, следовательно, без ограничений на его размер) autolayout не в состоянии решить, какой размер вещи должны быть быть. Это, вероятно, ошибка, но обходной путь достаточно тривиален, и, как я уже сказал, Обычно при переносе новых контроллеров вида в стек навигации вы инициализируете новые, и они исчезают, когда они выскакивают, так что вы никогда не увидите этого.