31

コントローラースタック全体を保持しないことを除いて、 UINavigationController に非常に似たカスタムコンテナーを実装しています。コンテナー コントローラーの topLayoutGuide に制約されている UINavigationBar があり、たまたま上部から 20 ピクセル離れていますが、これで問題ありません。

子View Controllerを追加してそのビューを階層に配置すると、そのtopLayoutGuideがIBに表示され、子View Controllerのビューのサブビューをレイアウトするために使用され、ナビゲーションバーの下部に表示されます。関連するドキュメントには、何をすべきかについての注意事項があります。

このプロパティの値は、具体的には、このプロパティをクエリしたときに返されるオブジェクトの長さプロパティの値です。この値は、次のように、ビュー コントローラーまたはそれを囲むコンテナー ビュー コントローラー (ナビゲーションまたはタブ バー コントローラーなど) によって制約されます。

  • コンテナView Controller内にないView Controllerは、このプロパティを制約して、表示されている場合はステータスバーの下部を示し、
    そうでない場合はView Controllerのビューの上端を示します。
  • コンテナ ビュー コントローラ内のビュー コントローラは、このプロパティの値を設定しません。代わりに、コンテナー ビュー コントローラーは値を制約して、次のことを示します。
    • ナビゲーション バーが表示されている場合は、ナビゲーション バーの下部
    • ステータス バーのみが表示されている場合は、ステータス バーの下部
    • ステータスバーもナビゲーションバーも表示されていない場合は、View Controller のビューの上端

しかし、topLayoutGuide と長さの両方のプロパティが読み取り専用であるため、「値を制限する」方法がよくわかりません。

子View Controllerを追加するためにこのコードを試しました:

[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 では、gamePhaseController に「Under Top Bars」と「Under Bottom Bars」を指定します。ビューの 1 つは特に上部のレイアウト ガイドに制限されていますが、デバイス上ではコンテナーのナビゲーション バーの下部から 20 ピクセル離れているように見えます...

この動作でカスタム コンテナ コントローラを実装する正しい方法は何ですか?

4

4 に答える 4

5

(更新: 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
于 2014-09-17T00:21:46.020 に答える
1

長さプロパティを手動で設定するのではなく、自動レイアウト、つまり NSLayoutConstraint オブジェクトを使用してレイアウト ガイドを制限する必要があることを意味していると思います。長さプロパティは、自動レイアウトを使用しないことを選択したクラスで使用できるようになっていますが、カスタム コンテナー ビュー コントローラーではこの選択ができないようです。

長さプロパティの値をUILayoutPriorityRequired.

NSLayoutAttributeHeightどちらか、またはNSLayoutAttributeBottomおそらくバインドするレイアウト属性がわかりません。

于 2014-04-17T01:54:06.027 に答える