Как установить верхнюю позицию topLayoutGuide для контроллера детского представления
Я реализую пользовательский контейнер, который очень похож на UINavigationController, за исключением того, что он не содержит весь стек контроллера. У него есть UINavigationBar, который ограничен контроллером контейнера topLayoutGuide, который, как оказалось, 20px сверху, что хорошо.
Когда я добавляю контроллер детского представления и помещаю его представление в иерархию, я хочу, чтобы его topLayoutGuide был замечен в IB и использовался для выделения дочерних элементов представления контроллера дочернего вида, которые появляются в нижней части моей панели навигации. Следует отметить, что должно быть сделано в соответствующей документации:
Значение этого свойства - это, в частности, значение длины свойство объекта, возвращаемого при запросе этого свойства. Эта значение ограничивается либо контроллером представления, либо его закрытием контроллер контейнера (например, панель навигации или вкладки контроллер):
- Контроллер вида, не находящийся в контроллере представления контейнера, ограничивает это свойство, чтобы указать нижнюю часть строки состояния, если она видна,
или указать верхний край представления контроллера вида. - Контроллер представления в контроллере представления контейнера не устанавливает это значение свойства. Вместо этого контроллер представления контейнера ограничивает значение, указывающее:
- Нижняя часть панели навигации, если отображается панель навигации
- Нижняя часть строки состояния, если видна только строка состояния
- Верхний край представления контроллеров просмотра, если не отображается ни строка состояния, ни панель навигации.
Но я не совсем понимаю, как "ограничить его значение", так как оба свойства topLayoutGuide и его длины являются readonly.
Я пробовал этот код для добавления контроллера детского представления:
[self addChildViewController:gamePhaseController];
UIView *gamePhaseControllerView = gamePhaseController.view;
gamePhaseControllerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentContainer addSubview:gamePhaseControllerView];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-0-[gamePhaseControllerView]-0-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(gamePhaseControllerView)];
NSLayoutConstraint *topLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.topLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.navigationBar
attribute:NSLayoutAttributeBottom
multiplier:1 constant:0];
NSLayoutConstraint *bottomLayoutGuideConstraint = [NSLayoutConstraint constraintWithItem:gamePhaseController.bottomLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
multiplier:1 constant:0];
[self.view addConstraint:topLayoutGuideConstraint];
[self.view addConstraint:bottomLayoutGuideConstraint];
[self.contentContainer addConstraints:horizontalConstraints];
[gamePhaseController didMoveToParentViewController:self];
_contentController = gamePhaseController;
В IB я определяю "Under Top Bars" и "Under Bottom Bars" для игрыPhaseController. Один из видов специально привязан к верхней направляющей макета, так или иначе на устройстве он выглядит 20px от нижней части панели навигации контейнера...
Каков правильный способ реализации пользовательского контроллера контейнера с таким поведением?
Ответы
Ответ 1
Насколько я смог рассказать после нескольких часов отладки, руководства по макету только для чтения и получены из частных классов, используемых для макета с ограничениями. Переопределение аксессуаров ничего не делает (хотя они и называются), и все это просто безумно раздражает.
Ответ 2
(UPDATE: теперь доступен как cocoapod, см. https://github.com/stefreak/TTLayoutSupport)
Рабочее решение - удалить ограничения макета apple и добавить собственные ограничения. Я сделал для этого небольшую категорию.
Вот код, но я предлагаю cocoapod. Он получил модульные тесты и, скорее всего, будет в курсе последних событий.
//
// UIViewController+TTLayoutSupport.h
//
// Created by Steffen on 17.09.14.
//
#import <UIKit/UIKit.h>
@interface UIViewController (TTLayoutSupport)
@property (assign, nonatomic) CGFloat tt_bottomLayoutGuideLength;
@property (assign, nonatomic) CGFloat tt_topLayoutGuideLength;
@end
-
#import "UIViewController+TTLayoutSupport.h"
#import "TTLayoutSupportConstraint.h"
#import <objc/runtime.h>
@interface UIViewController (TTLayoutSupportPrivate)
// recorded apple `UILayoutSupportConstraint` objects for topLayoutGuide
@property (nonatomic, strong) NSArray *tt_recordedTopLayoutSupportConstraints;
// recorded apple `UILayoutSupportConstraint` objects for bottomLayoutGuide
@property (nonatomic, strong) NSArray *tt_recordedBottomLayoutSupportConstraints;
// custom layout constraint that has been added to control the topLayoutGuide
@property (nonatomic, strong) TTLayoutSupportConstraint *tt_topConstraint;
// custom layout constraint that has been added to control the bottomLayoutGuide
@property (nonatomic, strong) TTLayoutSupportConstraint *tt_bottomConstraint;
// this is for NSNotificationCenter unsubscription (we can't override dealloc in a category)
@property (nonatomic, strong) id tt_observer;
@end
@implementation UIViewController (TTLayoutSupport)
- (CGFloat)tt_topLayoutGuideLength
{
return self.tt_topConstraint ? self.tt_topConstraint.constant : self.topLayoutGuide.length;
}
- (void)setTt_topLayoutGuideLength:(CGFloat)length
{
[self tt_ensureCustomTopConstraint];
self.tt_topConstraint.constant = length;
[self tt_updateInsets:YES];
}
- (CGFloat)tt_bottomLayoutGuideLength
{
return self.tt_bottomConstraint ? self.tt_bottomConstraint.constant : self.bottomLayoutGuide.length;
}
- (void)setTt_bottomLayoutGuideLength:(CGFloat)length
{
[self tt_ensureCustomBottomConstraint];
self.tt_bottomConstraint.constant = length;
[self tt_updateInsets:NO];
}
- (void)tt_ensureCustomTopConstraint
{
if (self.tt_topConstraint) {
// already created
return;
}
// recording does not work if view has never been accessed
__unused UIView *view = self.view;
// if topLayoutGuide has never been accessed it may not exist yet
__unused id<UILayoutSupport> topLayoutGuide = self.topLayoutGuide;
self.tt_recordedTopLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.topLayoutGuide];
NSAssert(self.tt_recordedTopLayoutSupportConstraints.count, @"Failed to record topLayoutGuide constraints. Is the controller view added to the view hierarchy?");
[self.view removeConstraints:self.tt_recordedTopLayoutSupportConstraints];
NSArray *constraints =
[TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
topLayoutGuide:self.topLayoutGuide];
// todo: less hacky?
self.tt_topConstraint = [constraints firstObject];
[self.view addConstraints:constraints];
// this fixes a problem with iOS7.1 (GH issue #2), where the contentInset
// of a scrollView is overridden by the system after interface rotation
// this should be safe to do on iOS8 too, even if the problem does not exist there.
__weak typeof(self) weakSelf = self;
self.tt_observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
__strong typeof(self) self = weakSelf;
[self tt_updateInsets:NO];
}];
}
- (void)tt_ensureCustomBottomConstraint
{
if (self.tt_bottomConstraint) {
// already created
return;
}
// recording does not work if view has never been accessed
__unused UIView *view = self.view;
// if bottomLayoutGuide has never been accessed it may not exist yet
__unused id<UILayoutSupport> bottomLayoutGuide = self.bottomLayoutGuide;
self.tt_recordedBottomLayoutSupportConstraints = [self findLayoutSupportConstraintsFor:self.bottomLayoutGuide];
NSAssert(self.tt_recordedBottomLayoutSupportConstraints.count, @"Failed to record bottomLayoutGuide constraints. Is the controller view added to the view hierarchy?");
[self.view removeConstraints:self.tt_recordedBottomLayoutSupportConstraints];
NSArray *constraints =
[TTLayoutSupportConstraint layoutSupportConstraintsWithView:self.view
bottomLayoutGuide:self.bottomLayoutGuide];
// todo: less hacky?
self.tt_bottomConstraint = [constraints firstObject];
[self.view addConstraints:constraints];
}
- (NSArray *)findLayoutSupportConstraintsFor:(id<UILayoutSupport>)layoutGuide
{
NSMutableArray *recordedLayoutConstraints = [[NSMutableArray alloc] init];
for (NSLayoutConstraint *constraint in self.view.constraints) {
// I think an equality check is the fastest check we can make here
// member check is to distinguish accidentally created constraints from _UILayoutSupportConstraints
if (constraint.firstItem == layoutGuide && ![constraint isMemberOfClass:[NSLayoutConstraint class]]) {
[recordedLayoutConstraints addObject:constraint];
}
}
return recordedLayoutConstraints;
}
- (void)tt_updateInsets:(BOOL)adjustsScrollPosition
{
// don't update scroll view insets if developer didn't want it
if (!self.automaticallyAdjustsScrollViewInsets) {
return;
}
UIScrollView *scrollView;
if ([self respondsToSelector:@selector(tableView)]) {
scrollView = ((UITableViewController *)self).tableView;
} else if ([self respondsToSelector:@selector(collectionView)]) {
scrollView = ((UICollectionViewController *)self).collectionView;
} else {
scrollView = (UIScrollView *)self.view;
}
if ([scrollView isKindOfClass:[UIScrollView class]]) {
CGPoint previousContentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + scrollView.contentInset.top);
UIEdgeInsets insets = UIEdgeInsetsMake(self.tt_topLayoutGuideLength, 0, self.tt_bottomLayoutGuideLength, 0);
scrollView.contentInset = insets;
scrollView.scrollIndicatorInsets = insets;
if (adjustsScrollPosition && previousContentOffset.y == 0) {
scrollView.contentOffset = CGPointMake(previousContentOffset.x, -scrollView.contentInset.top);
}
}
}
@end
@implementation UIViewController (TTLayoutSupportPrivate)
- (NSLayoutConstraint *)tt_topConstraint
{
return objc_getAssociatedObject(self, @selector(tt_topConstraint));
}
- (void)setTt_topConstraint:(NSLayoutConstraint *)constraint
{
objc_setAssociatedObject(self, @selector(tt_topConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSLayoutConstraint *)tt_bottomConstraint
{
return objc_getAssociatedObject(self, @selector(tt_bottomConstraint));
}
- (void)setTt_bottomConstraint:(NSLayoutConstraint *)constraint
{
objc_setAssociatedObject(self, @selector(tt_bottomConstraint), constraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)tt_recordedTopLayoutSupportConstraints
{
return objc_getAssociatedObject(self, @selector(tt_recordedTopLayoutSupportConstraints));
}
- (void)setTt_recordedTopLayoutSupportConstraints:(NSArray *)constraints
{
objc_setAssociatedObject(self, @selector(tt_recordedTopLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)tt_recordedBottomLayoutSupportConstraints
{
return objc_getAssociatedObject(self, @selector(tt_recordedBottomLayoutSupportConstraints));
}
- (void)setTt_recordedBottomLayoutSupportConstraints:(NSArray *)constraints
{
objc_setAssociatedObject(self, @selector(tt_recordedBottomLayoutSupportConstraints), constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)setTt_observer:(id)tt_observer
{
objc_setAssociatedObject(self, @selector(tt_observer), tt_observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)tt_observer
{
return objc_getAssociatedObject(self, @selector(tt_observer));
}
-
//
// TTLayoutSupportConstraint.h
//
// Created by Steffen on 17.09.14.
//
#import <UIKit/UIKit.h>
@interface TTLayoutSupportConstraint : NSLayoutConstraint
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide;
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide;
@end
-
//
// TTLayoutSupportConstraint.m
//
// Created by Steffen on 17.09.14.
//
#import "TTLayoutSupportConstraint.h"
@implementation TTLayoutSupportConstraint
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view topLayoutGuide:(id<UILayoutSupport>)topLayoutGuide
{
return @[
[TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0],
[TTLayoutSupportConstraint constraintWithItem:topLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0],
];
}
+ (NSArray *)layoutSupportConstraintsWithView:(UIView *)view bottomLayoutGuide:(id<UILayoutSupport>)bottomLayoutGuide
{
return @[
[TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:0.0],
[TTLayoutSupportConstraint constraintWithItem:bottomLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:view
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0.0],
];
}
@end
Ответ 3
Я думаю, что они означают, что вы должны ограничить руководство по макетам, используя autolayout, т.е. объект NSLayoutConstraint, вместо того, чтобы вручную установить свойство length. Свойство length доступно для классов, которые предпочитают не использовать автозапуск, но, похоже, пользовательские контроллеры контейнеров не имеют этого выбора.
Я предполагаю, что наилучшей практикой является приоритет ограничения в контроллере представления контейнера, который "устанавливает" значение свойства length на UILayoutPriorityRequired
.
Я не уверен, какой атрибут компоновки вы свяжете, возможно, NSLayoutAttributeHeight
или NSLayoutAttributeBottom
.
Ответ 4
В контроллере родительского представления
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
for (UIViewController * childViewController in self.childViewControllers) {
// Pass the layouts to the child
if ([childViewController isKindOfClass:[MyCustomViewController class]]) {
[(MyCustomViewController *)childViewController parentTopLayoutGuideLength:self.topLayoutGuide.length parentBottomLayoutGuideLength:self.bottomLayoutGuide.length];
}
}
}
а затем передать значения дочерним элементам, вы можете иметь собственный класс, как в моем примере, протокол, или вы можете получить доступ к представлению прокрутки из дочерней иерархии