短縮版:
IOS7でカスタムトランジションおよびUINavigationController
と組み合わせて使用すると、自動レイアウトトップレイアウトガイドで問題が発生します。具体的には、トップレイアウトガイドとテキストビューの間の制約が守られていません。誰もこの問題に遭遇しましたか?
ロングバージョン:
私は、次のようなビューをレンダリングする制約(つまり、上、下、左、右)を明確に定義したシーンを持っています:
ただし、Navigation Controllerのカスタムトランジションでこれを使用すると、上部レイアウトガイドの上部の制約がオフになり、上部レイアウトガイドが下部ではなく画面上部にあるかのようにレンダリングされますNavigation Controllerの:
カスタムトランジションを使用すると、Navigation Controllerの「トップレイアウトガイド」が混乱しているように見えます。残りの制約は正しく適用されています。そして、デバイスを回転させて再び回転させると、すべてが突然正しくレンダリングされるため、制約が適切に定義されていないことは問題ではないようです。同様に、カスタムトランジションをオフにすると、ビューが正しくレンダリングされます。
そうは言っても、 _autolayoutTrace
は、UILayoutGuide
オブジェクトがAMBIGUOUS LAYOUT
、実行時:
(lldb) po [[UIWindow keyWindow] _autolayoutTrace]
しかし、これらのレイアウトガイドは、制約が欠落していないことを確認したにもかかわらず、それらを見ると常に曖昧であると報告されますコントロールのおよびそれらのために同じことを行う)。
移行をどの程度正確に行っているかという観点から、UIViewControllerAnimatedTransitioning
メソッドでanimationControllerForOperation
に準拠するオブジェクトを指定しました。
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush)
return [[PushAnimator alloc] init];
return nil;
}
そして
@implementation PushAnimator
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
CGFloat width = fromViewController.view.frame.size.width;
toViewController.view.transform = CGAffineTransformMakeTranslation(width, 0);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.transform = CGAffineTransformMakeTranslation(-width / 2.0, 0);
toViewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end
また、上記のレンディションを行って、ビューのframe
ではなくtransform
を設定し、同じ結果を得ました。
また、layoutIfNeeded
を呼び出して、制約が再適用されることを手動で確認しようとしました。 setNeedsUpdateConstraints
、setNeedsLayout
なども試しました。
結論として、トップレイアウトガイドを使用する制約を持つNavigation Controllerのカスタムトランジションと結婚した人はいますか?
topLayoutGuide
の高さの制約を修正することでこれを解決しました。 edgesForExtendedLayout
を調整することはオプションではありませんでした。ナビゲーションバーの下に目的のビューが必要でしたが、topLayoutGuide
を使用してサブビューをレイアウトできるようにしたからです。
プレイ中の制約を直接検査すると、iOSがtopLayoutGuide
に、Navigation ControllerのNavigation Barの高さと等しい値を持つ高さ制約を追加することがわかります。ただし、iOS 7では、カスタムアニメーショントランジションを使用すると、制約の高さが0のままになります。iOS8ではこれを修正しました。
これは、制約を修正するために思いついた解決策です(Swiftにありますが、同等のものはObj-Cで動作するはずです)。iOS7および8で動作することをテストしました。
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!.view
let destinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
destinationVC.view.frame = transitionContext.finalFrameForViewController(destinationVC)
let container = transitionContext.containerView()
container.addSubview(destinationVC.view)
// Custom transitions break topLayoutGuide in iOS 7, fix its constraint
if let navController = destinationVC.navigationController {
for constraint in destinationVC.view.constraints() as [NSLayoutConstraint] {
if constraint.firstItem === destinationVC.topLayoutGuide
&& constraint.firstAttribute == .Height
&& constraint.secondItem == nil
&& constraint.constant == 0 {
constraint.constant = navController.navigationBar.frame.height
}
}
}
// Perform your transition animation here ...
}
次の行を追加して問題を解決しました:
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
に:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext fromVC:(UIViewController *)fromVC toVC:(UIViewController *)toVC fromView:(UIView *)fromView toView:(UIView *)toView {
// Add the toView to the container
UIView* containerView = [transitionContext containerView];
[containerView addSubview:toView];
[containerView sendSubviewToBack:toView];
// animate
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
fromView.alpha = 0.0;
} completion:^(BOOL finished) {
if ([transitionContext transitionWasCancelled]) {
fromView.alpha = 1.0;
} else {
// reset from- view to its original state
[fromView removeFromSuperview];
fromView.alpha = 1.0;
}
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
Appleの[finalFrameForViewController]のドキュメントから: https://developer.Apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerContextTransitioning_protocol/#//Apple_ref/occ/intfm/UIViewControllerContextTransitioning/finalFrameForViewController :
私はまったく同じ問題に苦労しました。これをtoViewControllerのviewDidLoadに入れると、本当に役立ちました。
self.edgesForExtendedLayout = UIRectEdgeNone;
これは私のすべての問題を解決しませんでしたし、私はまだより良いアプローチを探していますが、これにより確かに少し簡単になりました。
次のコードをto viewDidLoad
に追加するだけです
self.extendedLayoutIncludesOpaqueBars = YES;
参考までに、私は Alexの答え のバリエーションを採用し、animateTransition
メソッドでトップレイアウトガイドの高さ制約定数をプログラムで変更しました。 Objective-Cのレンディションを共有するためにのみ投稿しています(そしてconstant == 0
test)。
CGFloat navigationBarHeight = toViewController.navigationController.navigationBar.frame.size.height;
for (NSLayoutConstraint *constraint in toViewController.view.constraints) {
if (constraint.firstItem == toViewController.topLayoutGuide
&& constraint.firstAttribute == NSLayoutAttributeHeight
&& constraint.secondItem == nil
&& constraint.constant < navigationBarHeight) {
constraint.constant += navigationBarHeight;
}
}
ありがとう、アレックス。
@Robが述べたように、topLayoutGuide
でカスタム遷移を使用する場合、UINavigationController
は信頼できません。独自のレイアウトガイドを使用して、これを回避しました。この デモプロジェクト で実際のコードを確認できます。ハイライト:
カスタムレイアウトガイドのカテゴリ:
@implementation UIViewController (hp_layoutGuideFix)
- (BOOL)hp_usesTopLayoutGuideInConstraints
{
return NO;
}
- (id<UILayoutSupport>)hp_topLayoutGuide
{
id<UILayoutSupport> object = objc_getAssociatedObject(self, @selector(hp_topLayoutGuide));
return object ? : self.topLayoutGuide;
}
- (void)setHp_topLayoutGuide:(id<UILayoutSupport>)hp_topLayoutGuide
{
HPLayoutSupport *object = objc_getAssociatedObject(self, @selector(hp_topLayoutGuide));
if (object != nil && self.hp_usesTopLayoutGuideInConstraints)
{
[object removeFromSuperview];
}
HPLayoutSupport *layoutGuide = [[HPLayoutSupport alloc] initWithLength:hp_topLayoutGuide.length];
if (self.hp_usesTopLayoutGuideInConstraints)
{
[self.view addSubview:layoutGuide];
}
objc_setAssociatedObject(self, @selector(hp_topLayoutGuide), layoutGuide, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
HPLayoutSupport
は、レイアウトガイドとして機能するクラスです。クラッシュを避けるためにUIView
サブクラスである必要があります(これがUILayoutSupport
インターフェイスの一部ではないのはなぜだろうか)。
@implementation HPLayoutSupport {
CGFloat _length;
}
- (id)initWithLength:(CGFloat)length
{
self = [super init];
if (self)
{
self.translatesAutoresizingMaskIntoConstraints = NO;
self.userInteractionEnabled = NO;
_length = length;
}
return self;
}
- (CGSize)intrinsicContentSize
{
return CGSizeMake(1, _length);
}
- (CGFloat)length
{
return _length;
}
@end
UINavigationControllerDelegate
は、移行前にレイアウトガイドを「修正」する役割を果たします。
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
toVC.hp_topLayoutGuide = fromVC.hp_topLayoutGuide;
id <UIViewControllerAnimatedTransitioning> animator;
// Initialise animator
return animator;
}
最後に、UIViewController
は制約でtopLayoutGuide
の代わりにhp_topLayoutGuide
を使用し、hp_usesTopLayoutGuideInConstraints
をオーバーライドすることでこれを示します。
- (void)updateViewConstraints
{
[super updateViewConstraints];
id<UILayoutSupport> topLayoutGuide = self.hp_topLayoutGuide;
// Example constraint
NSDictionary *views = NSDictionaryOfVariableBindings(_imageView, _dateLabel, topLayoutGuide);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[topLayoutGuide][_imageView(240)]-8-[_dateLabel]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:views];
[self.view addConstraints:constraints];
}
- (BOOL)hp_usesTopLayoutGuideInConstraints
{
return YES;
}
それが役に立てば幸い。
道を見つけた。まず、コントローラーの「エッジを拡張」プロパティのチェックを外します。ナビゲーションバーが暗い色になった後。ビューをコントローラーに追加し、上部と下部のLayoutConstraint -100を設定します。次に、ビューのclipsubviewプロパティをnoにします(navigaionbar transculentエフェクト用)。そのための私の英語の悪い悲しみ。 :)
私も同じ問題を抱えていたため、自分のtopLayoutガイドビューを実装し、topLayoutGuideではなく、それに制約を加えました。理想的ではありません。誰かが立ち往生している場合にのみここに投稿し、迅速なハッキングソリューションを探しています http://www.github.com/stringcode86/SCTopLayoutGuide
ここに私が使用しているsimpleソリューションがあります:- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
のセットアップ段階で、 "from"と "to" viewController.view.frameを手動で設定します。 Origin.y = navigationController.navigationBar.frame.size.height。これにより、自動レイアウトビューが期待どおりに垂直に配置されます。
擬似コードを除いて(たとえば、デバイスがiOS7を実行しているかどうかを判断する独自の方法がある可能性があります)、これは私の方法のように見えます:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
CGAffineTransform destinationTransform;
UIViewController *targetVC;
CGFloat adjustmentForIOS7AutoLayoutBug = 0.0f;
// We're doing a view controller POP
if(self.isViewControllerPop)
{
targetVC = fromViewController;
[container insertSubview:toViewController.view belowSubview:fromViewController.view];
// Only need this auto layout hack in iOS7; it's fixed in iOS8
if(_device_is_running_iOS7_)
{
adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
[toViewController.view setFrameOriginY:adjustmentForIOS7AutoLayoutBug];
}
destinationTransform = CGAffineTransformMakeTranslation(fromViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
}
// We're doing a view controller Push
else
{
targetVC = toViewController;
[container addSubview:toViewController.view];
// Only need this auto layout hack in iOS7; it's fixed in iOS8
if(_device_is_running_iOS7_)
{
adjustmentForIOS7AutoLayoutBug = toViewController.navigationController.navigationBar.frame.size.height;
}
toViewController.view.transform = CGAffineTransformMakeTranslation(toViewController.view.bounds.size.width,adjustmentForIOS7AutoLayoutBug);
destinationTransform = CGAffineTransformMakeTranslation(0.0f,adjustmentForIOS7AutoLayoutBug);
}
[UIView animateWithDuration:_animation_duration_
delay:_animation_delay_if_you_need_one_
options:([transitionContext isInteractive] ? UIViewAnimationOptionCurveLinear : UIViewAnimationOptionCurveEaseOut)
animations:^(void)
{
targetVC.view.transform = destinationTransform;
}
completion:^(BOOL finished)
{
[transitionContext completeTransition:([transitionContext transitionWasCancelled] ? NO : YES)];
}];
}
この例に関するいくつかのボーナス事項:
[UIView animateWithDuration:...]
完了ブロックは、完了およびキャンセルされたカスタム遷移を処理する正しい方法を示しています。この小さな一口は、古典的なヘッドスラップの瞬間でした。それが他の誰かに役立つことを願っています。最後に、私が知る限り、これはiOS8で修正されたiOS7のみの問題であることを指摘したいと思います:iOS7で壊れている私のカスタムビューコントローラーの移行は、変更なしでiOS8でうまく機能します。そうは言っても、これが表示されていることを確認する必要があります。もしそうなら、iOS7.xを実行しているデバイスでのみ修正を実行してください。上記のコード例でわかるように、デバイスがiOS7.xを実行していない限り、y調整値は0.0fです。
試してください:
self.edgesforextendedlayout=UIRectEdgeNone
または、Navigationbarを不透明に設定し、背景画像またはbackgroundcolorをnavigationbarに設定するだけです
私はこれと同じ問題にぶつかりましたが、UINavigationController
を使用せず、ビューをtopLayoutGuide
の外に配置するだけでした。レイアウトは最初に表示されたときに正しくなり、別のビューに遷移します。その後、終了して最初のビューに戻ると、topLayoutGuide
がなくなったためレイアウトが壊れます。
この問題を解決するには、移行前にセーフエリアのインセットをキャプチャし、制約を調整するのではなく、viewControllerのadditionalSafeAreaInsets
に設定することで再実装します。
レイアウトコードを調整したり制約を検索したりする必要がなく、以前あったスペースを再実装するだけでよいため、このソリューションがうまく機能することがわかりました。実際にadditionalSafeAreaInsets
プロパティを使用している場合、これはより困難になる可能性があります。
TransitionManagerの作成時に存在する安全なインセットをキャプチャするために、transitionManagerに変数を追加しました。
_class MyTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
private var container:UIView?
private var safeInsets:UIEdgeInsets?
...
_
次に、移行を開始するときに、これらのインセットを保存します。
_ let toView = viewControllers.to.view
let fromView = viewControllers.from.view
if #available(iOS 11.0, *) {
safeInsets = toView.safeAreaInsets
}
_
IPhone Xの場合、これはUIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
のようなものになります
終了すると、入口から遷移した同じビューのインセットは_.zero
_になるので、キャプチャしたインセットをviewControllerのadditionalSafeAreaInsets
に追加し、ビューに設定しますレイアウトを更新します。アニメーションが完了したら、additionalSafeAreaInsets
を_.zero
_にリセットします。
_ if #available(iOS 11.0, *) {
if safeInsets != nil {
viewControllers.to.additionalSafeAreaInsets = safeInsets!
}
}
...then in the animation completion block
if #available(iOS 11.0, *) {
if self.safeInsets != nil {
viewControllers.to.additionalSafeAreaInsets = .zero
}
}
transitionContext.completeTransition(true)
_