it-swarm.com.ru

Как установить позицию topLayoutGuide для контроллера дочернего представления

Я реализую пользовательский контейнер, который очень похож на UINavigationController за исключением того, что он не содержит весь стек контроллера. Он имеет UINavigationBar, который ограничен topLayoutGuide контроллера контейнера, который находится на 20 пикселей сверху, что нормально. 

Когда я добавляю дочерний контроллер представления и помещаю его представление в иерархию, я хочу, чтобы его topLayoutGuide просматривался в IB и использовался для разметки подпредставлений представления дочернего контроллера, чтобы он отображался внизу моей панели навигации. В соответствующей документации есть примечание о том, что необходимо сделать:

Значение этого свойства, в частности, значение длины свойство объекта, возвращаемого при запросе этого свойства. Это значение ограничено либо контроллером представления, либо его включением Контроллер контейнера (например, панель навигации или панель вкладок Контроллер):

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

Но я не совсем понимаю, как «ограничить его значение», так как свойства topLayoutGuide и его длина доступны только для чтения.

Я пробовал этот код для добавления дочернего контроллера представления:

[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 Bars Bars» и «Under Bottom Bars» для игры PhaseController. Одно из представлений специально ограничено верхним руководством по макету, в любом случае на устройстве оно выглядит как 20px от нижней части навигационной панели контейнера ...

Как правильно реализовать собственный контейнерный контроллер с таким поведением?

31
Danchoys

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

29
Stefan Fisk

(ОБНОВЛЕНИЕ: теперь доступно как cocoapod, см. https://github.com/stefreak/TTLayoutSupport )

Рабочим решением является удаление ограничений макета Apple и добавление собственных ограничений. Я сделал небольшую категорию для этого.

Вот код - но я предлагаю кокосовый сок. У него есть модульные тесты, и, скорее всего, он будет в курсе.

//
//  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's `UILayoutSupportConstraint` objects for topLayoutGuide
@property (nonatomic, strong) NSArray *tt_recordedTopLayoutSupportConstraints;

// recorded Apple's `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's 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's 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
5
stefreak

Я думаю, что они означают, что вы должны ограничить направляющие макетов, используя autolayout, то есть объект NSLayoutConstraint, вместо того, чтобы вручную устанавливать свойство длины. Свойство length доступно для классов, которые не используют autolayout, но, похоже, с пользовательскими контроллерами представления контейнера у вас нет этого выбора.

Я полагаю, что лучше всего делать приоритет ограничения в контроллере представления контейнера, который «устанавливает» значение свойства длины в UILayoutPriorityRequired.

Я не уверен, какой атрибут макета вы бы связали, NSLayoutAttributeHeight или NSLayoutAttributeBottom, вероятно.

1
jamesmoschou

В родительском контроллере представления

- (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];
        }

    }

}

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

0
Peter Lapisu