tutorial objective life example containerviewcontroller container ios objective-c uiviewcontroller ios7 autolayout

ios - objective - Cómo establecer la posición topLayoutGuide para el controlador de vista infantil



lifecycle viewcontroller ios (4)

Estoy implementando un contenedor personalizado que es bastante similar a UINavigationController a excepción de que no contiene toda la pila de controladores. Tiene una UINavigationBar que está restringida a topLayoutGuide del controlador de contenedor, que es 20px desde la parte superior, lo cual está bien.

Cuando agrego un controlador de vista secundario y coloco su vista en la jerarquía, quiero que se vea su topLayoutGuide en IB y se use para diseñar las subvistas de la vista del controlador de vista secundaria para que aparezcan en la parte inferior de la barra de navegación. Hay una nota de lo que se debe hacer en la documentación relevante:

El valor de esta propiedad es, específicamente, el valor de la propiedad de longitud del objeto devuelto cuando consulta esta propiedad. Este valor está restringido por el controlador de vista o por su controlador de vista contenedor contenedor (como un controlador de navegación o barra de pestañas), de la siguiente manera:

  • Un controlador de vista que no está dentro de un controlador de vista de contenedor limita esta propiedad para indicar la parte inferior de la barra de estado, si está visible,
    o bien, para indicar el borde superior de la vista del controlador de vista.
  • Un controlador de vista dentro de un controlador de vista de contenedor no establece el valor de esta propiedad. En cambio, el controlador de vista contenedor limita el valor para indicar:
    • La parte inferior de la barra de navegación, si una barra de navegación es visible
    • La parte inferior de la barra de estado, si solo está visible una barra de estado
    • El borde superior de la vista del controlador de vista, si ni una barra de estado ni una de navegación están visibles

Pero no entiendo muy bien cómo "restringir su valor", ya que tanto la propiedad topLayoutGuide como sus propiedades de longitud son de solo lectura.

He intentado este código para agregar un controlador de vista hijo:

[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;

En el IB, especifico "Under Top Bars" y "Under Bottom Bars" para el juego PhaseController. Una de las vistas está específicamente restringida a la guía de diseño superior; de todos modos, en el dispositivo parece estar a 20 píxeles de la parte inferior de la barra de navegación del contenedor ...

¿Cuál es la forma correcta de implementar un controlador de contenedor personalizado con este comportamiento?


(ACTUALIZACIÓN: ahora disponible como cocoapod, consulte https://github.com/stefreak/TTLayoutSupport )

Una solución de trabajo es eliminar las restricciones de diseño de apple y agregar sus propias restricciones. Hice una pequeña categoría para esto.

Aquí está el código, pero sugiero el cocoapod. Tiene pruebas unitarias y es más probable que esté actualizado.

// // 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


Creo que quieren decir que debe restringir las guías de diseño usando el autolayout, es decir, un objeto NSLayoutConstraint, en lugar de establecer manualmente la propiedad de longitud. La propiedad de longitud está disponible para las clases que optan por no usar el autolayout, pero parece que con los controladores de vista de contenedor personalizados no tiene esta opción.

Supongo que la mejor práctica es establecer la prioridad de la restricción en el controlador de vista de contenedor que "establece" el valor de la propiedad de longitud en UILayoutPriorityRequired .

No estoy seguro de qué atributo de diseño vincularía, ya sea NSLayoutAttributeHeight o NSLayoutAttributeBottom probablemente.


En el controlador de vista padre

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

y que pasar los valores a los niños, puede tener una clase personalizada como en mi ejemplo, un protocolo, o puede acceder a la vista de desplazamiento desde la jerarquía del niño


Por lo que he podido decir después de horas de depuración, las guías de diseño son de solo lectura y se derivan de las clases privadas utilizadas para el diseño basado en restricciones. Anular los accesadores no hace nada (aunque se llamen), y todo es simplemente irritante.