Comment définir la position de topLayoutGuide pour le contrôleur de vue enfant
J'implémente un conteneur personnalisé qui est assez similaire à UINavigationController, à la différence qu'il ne contient pas toute la pile de contrôleurs. Il a un UINavigationBar qui est contraint à topLayoutGuide du contrôleur de conteneur, qui se trouve être à 20px du haut, ce qui est OK.
Lorsque j'ajoute un contrôleur de vue enfant et le mets dans la hiérarchie, je souhaite que son topLayoutGuide soit vu dans IB et utilisé pour la présentation des sous-vues de la vue du contrôleur de vue enfant en bas de ma barre de navigation. Il y a une note de ce qui doit être fait dans la documentation pertinente:
La valeur de cette propriété est, en particulier, la valeur de la longueur propriété de l'objet renvoyé lorsque vous interrogez cette propriété. Ce La valeur est contrainte soit par le contrôleur de vue, soit par son enveloppe contrôleur de vue du conteneur (tel qu'un contrôleur de navigation ou de barre de navigation ), comme suit:
- Un contrôleur de vue ne se trouvant pas dans un conteneur, le contrôleur de vue contraint cette propriété à indiquer le bas de la barre d'état, si elle est visible.
ou bien pour indiquer le bord supérieur de la vue du contrôleur de vue.- Un contrôleur de vue dans un contrôleur de vue de conteneur ne définit pas la valeur de cette propriété. Au lieu de cela, le contrôleur de vue du conteneur contraint la valeur à indiquer:
- Le bas de la barre de navigation, si une barre de navigation est visible
- Le bas de la barre d'état, si seulement une barre d'état est visible
- Bord supérieur de la vue du contrôleur de vue, si ni une barre d’état ni une barre de navigation ne sont visibles
Mais je ne comprends pas très bien comment "contraindre sa valeur" puisque topLayoutGuide et ses propriétés de longueur sont en lecture seule.
J'ai essayé ce code pour ajouter un contrôleur de vue enfant:
[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;
Dans l'IB, je spécifie "Under Top Bars" et "Under Bottom Bars" pour le gamePhaseController. L'une des vues est spécifiquement limitée au guide de disposition supérieur. Quoi qu'il en soit, sur le périphérique, elle semble être à 20 pixels de la partie inférieure de la barre de navigation du conteneur ...
Quelle est la bonne façon d'implémenter un contrôleur de conteneur personnalisé avec ce comportement?
Pour autant que je sache, après des heures de débogage, les guides de présentation sont en lecture seule et dérivés des classes privées utilisées pour la présentation par contraintes. Remplacer les accesseurs ne fait rien (même s’ils sont appelés), et c’est tout simplement terriblement ennuyant.
(UPDATE: maintenant disponible en tant que cocoapod, voir https://github.com/stefreak/TTLayoutSupport )
Une solution efficace consiste à supprimer les contraintes de mise en page d’Apple et à ajouter vos propres contraintes. J'ai fait une petite catégorie pour cela.
Voici le code - mais je suggère le cocoapod. Il a des tests unitaires et est plus susceptible d'être à jour.
//
// 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
Je pense qu'ils signifient que vous devez contraindre les guides de mise en page à l'aide de autolayout, c'est-à-dire un objet NSLayoutConstraint, au lieu de définir manuellement la propriété length La propriété length est disponible pour les classes qui choisissent de ne pas utiliser autolayout, mais il semble qu'avec les contrôleurs de vue de conteneur personnalisés, vous ne disposez pas de ce choix.
Je suppose que la meilleure pratique consiste à définir la priorité de la contrainte dans le contrôleur de vue du conteneur qui "définit" la valeur de la propriété length sur UILayoutPriorityRequired
.
Je ne sais pas quel attribut de mise en page vous lieriez, probablement NSLayoutAttributeHeight
ou NSLayoutAttributeBottom
.
Dans le contrôleur de vue parent
- (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];
}
}
}
et que transmettre les valeurs aux enfants, vous pouvez avoir une classe personnalisée comme dans mon exemple, un protocole, ou vous pouvez peut-être accéder à la vue de défilement à partir de la hiérarchie de l'enfant