web-dev-qa-db-ja.com

iOS 7、デフォルトのinteractivePopGestureRecognizerを使用して高速にスワイプするとUINavigationBarが破損する

私は立ち往生している問題がありますが、なぜそれが起こるのかさえわかりません。スタック上の詳細コントローラーをプッシュし、デフォルトの左エッジinteractivePopGestureRecognizerを使用して非常にすばやく戻ると、親/ルートビューコントローラーのUINavigationBarが破損しているか、組み込みのように見えます。 iOSの移行メカニズムには、詳細ビューがなくなった後にリセットするという仕事をする時間がありませんでした。また、明確にするために、この「破損した」UINavigationBarのすべてはまだタッチ可能であり、私の親/ルートビューコントローラーのすべてが完全に機能します。

ソースコードがないために反対票を投じる人のために:ソースコードはありません!これはAppleバグです!

このUINavigationBarを、親/ルートビューコントローラーのviewDidAppearメソッドが呼び出されたときの状態にリセットする方法はありますか?

左端interactivePopGestureRecognizerを使用する代わりに左上の戻るボタンをタップしても、このバグは発生しないことに注意してください。

編集:NSLogを追加して、親/ルートビューコントローラーのviewDidAppearでnavigationBarのサブビューカウントを確認しました。カウントは常に同じで、破損しているかどうかに関係なく、ポップされたコントローラーがなぜ大混乱を引き起こしているのかを知りたいです。 UINavigationBar

よろしければ、よろしくお願いします!ありがとうございました。

外観のスクリーンショットを添付しました。バックシェブロンは親/ルートビューコントローラーの一部ではなく、スタックからポップされたものの一部であることに注意してください。 Testing123は、親/ルートビューコントローラーのタイトルであり、スタックからポップされたもののタイトルではありません。頭と歯車のアイコンは、親/ルートビューコントローラーの一部です。

編集:私はこのような何かが問題を解決できると思っていましたが、それは解決しないことがわかりました、そしてIMOも本当に悪い経験です。これは私が探している種類の解決策ではありません。これを正しく解決できるように、私は大きな報奨金を投稿しています! ????。この奇妙なUIの動作を本番品質のアプリに含めることはできません。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self.navigationController pushViewController:[UIViewController new] animated:NO];
    [self.navigationController popToRootViewControllerAnimated:YES];
}

enter image description here

39
klcjr89

TL; DR

UIViewControllerにカテゴリを作成しました。これにより、この問題が修正されることを願っています。デバイスでナビゲーションバーの破損を実際に再現することはできませんが、シミュレーターでかなり頻繁に再現できます。このカテゴリで問題が解決します。うまくいけば、それはまたあなたのためにデバイス上でそれを解決します。

問題と解決策

何が原因なのか正確にはわかりませんが、ナビゲーションバーのサブビューのレイヤーのアニメーションが2回実行されているか、完全に完了していないか...何かが発生しているようです。とにかく、これらのサブビューにいくつかのアニメーションを追加するだけで、それらを本来あるべき場所に強制的に戻すことができることがわかりました(適切な不透明度、色など)。秘訣は、ViewControllerのtransitionCoordinatorオブジェクトを使用して、いくつかのイベントにフックすることです。つまり、指を離してインタラクティブなポップジェスチャレコグナイザーが終了し、残りのアニメーションが開始したときに発生するイベントです。次に、アニメーションの非インタラクティブな半分が終了したときに発生するイベント。

transitionCoordinatorのいくつかのメソッド、具体的にはnotifyWhenInteractionEndsUsingBlock:animateAlongsideTransition:completion:を使用して、これらのイベントにフックできます。前者では、ナビゲーションバーのサブビューのレイヤーの現在のすべてのアニメーションのコピーを作成し、それらをわずかに変更して保存します。これにより、後でアニメーションの非インタラクティブ部分が終了したときに適用できるようになります。これは完了ブロックにあります。これらの2つの方法の後者の。

概要

  1. トランジションのインタラクティブな部分がいつ終了するかを聞きます
  2. ナビゲーションバーのすべてのビューのレイヤーのアニメーションを収集します
  3. これらのアニメーションを少しコピーして変更します(fromValueをtoValueと同じものに設定し、期間をゼロに設定し、その他いくつかのことを行います)
  4. トランジションの非インタラクティブ部分がいつ終了するかをリッスンします
  5. コピー/変更したアニメーションをビューのレイヤーに適用し直します

コード

そして、これがUIViewControllerカテゴリのコードです。

@interface UIViewController (FixNavigationBarCorruption)

- (void)fixNavigationBarCorruption;

@end

@implementation UIViewController (FixNavigationBarCorruption)

/**
 * Fixes a problem where the navigation bar sometimes becomes corrupt
 * when transitioning using an interactive transition.
 *
 * Call this method in your view controller's viewWillAppear: method
 */
- (void)fixNavigationBarCorruption
{
    // Get our transition coordinator
    id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;

    // If we have a transition coordinator and it was initially interactive when it started,
    // we can attempt to fix the issue with the nav bar corruption.
    if ([coordinator initiallyInteractive]) {

        // Use a map table so we can map from each view to its animations
        NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory
                                                         valueOptions:NSMapTableStrongMemory
                                                             capacity:0];

        // This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer
        [coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {

            // Loop through our nav controller's nav bar's subviews
            for (UIView *view in self.navigationController.navigationBar.subviews) {

                NSArray *animationKeys = view.layer.animationKeys;
                NSMutableArray *anims = [NSMutableArray array];

                // Gather this view's animations
                for (NSString *animationKey in animationKeys) {
                    CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey];

                    // In case any other kind of animation somehow gets added to this view, don't bother with it
                    if ([anim isKindOfClass:[CABasicAnimation class]]) {

                        // Make a pseudo-hard copy of each animation.
                        // We have to make a copy because we cannot modify an existing animation.
                        CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath];

                        // CABasicAnimation properties
                        // Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value
                        animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath];
                        animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath];
                        animCopy.byValue = anim.byValue;

                        // CAPropertyAnimation properties
                        animCopy.additive = anim.additive;
                        animCopy.cumulative = anim.cumulative;
                        animCopy.valueFunction = anim.valueFunction;

                        // CAAnimation properties
                        animCopy.timingFunction = anim.timingFunction;
                        animCopy.delegate = anim.delegate;
                        animCopy.removedOnCompletion = anim.removedOnCompletion;

                        // CAMediaTiming properties
                        animCopy.speed = anim.speed;
                        animCopy.repeatCount = anim.repeatCount;
                        animCopy.repeatDuration = anim.repeatDuration;
                        animCopy.autoreverses = anim.autoreverses;
                        animCopy.fillMode = anim.fillMode;

                        // We want our new animations to be instantaneous, so set the duration to zero.
                        // Also set both the begin time and time offset to 0.
                        animCopy.duration = 0;
                        animCopy.beginTime = 0;
                        animCopy.timeOffset = 0;

                        [anims addObject:animCopy];
                    }
                }

                // Associate the gathered animations with each respective view
                [mapTable setObject:anims forKey:view];
            }
        }];

        // The completion block here gets run after the view controller transition animation completes (or fails)
        [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {

            // Iterate over the mapTable's keys (views)
            for (UIView *view in mapTable.keyEnumerator) {

                // Get the modified animations for this view that we made when the interactive portion of the transition finished
                NSArray *anims = [mapTable objectForKey:view];

                // ... and add them back to the view's layer
                for (CABasicAnimation *anim in anims) {
                    [view.layer addAnimation:anim forKey:anim.keyPath];
                }
            }
        }];
    }
}

@end

次に、ViewControllerのviewWillAppear:メソッドでこのメソッドを呼び出すだけです(テストプロジェクトの場合は、ViewControllerクラスになります)。

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self fixNavigationBarCorruption];
}
20

デバッグコンソール、Instruments and Revealを使用してこの問題をしばらく調査した後、次のことがわかりました。

1)シミュレータでは、プロファイル/自動化テンプレートを使用して次のスクリプトを追加すると、バグを毎回再現できます。

var target = UIATarget.localTarget();
var appWindow = target.frontMostApp().mainWindow();
appWindow.buttons()[0].tap();
target.delay(1);
target.flickFromTo({x:2, y: 100}, {x:160, y: 100});

2)実際のデバイス(iPhone 5s、iOS 7.1)では、このスクリプトによってバグが発生することはありません。フリック座標と遅延のさまざまなオプションを試しました。

3)UINavigationBarの構成:

_UINavigationBarBackground (doesn't seem to be related to the bug)
      _UIBackdropView
           _UIBackgropEffectView
           UIView
      UIImageView
UINavigationItemView
      UILabel (visible in the bug)
_UINavigationBarBackIndicatorView (visible in the bug)

4)バグが発生すると、UILabelは半分透明になり、位置が正しくなくなりますが、UILabelの実際のプロパティは正しいです(アルファ:1、通常の状況と同じフレーム)。また、_UINavigationBarBackIndicatorViewの外観は実際のプロパティに対応していません。アルファは0ですが表示されます。

このことから、これはSimulatorのバグであり、コードから何かが間違っていることを検出することすらできないと結論付けます。

だから@ troop231-これはデバイスでも起こると100%確信していますか?

3
Andris Zalitis

キーコンセプト

ビューコントローラを押すときにジェスチャレコグナイザを無効にし、ビューが表示されたときに有効にします。

一般的な解決策:サブクラス化

UINavigationControllerUIViewControllerをサブクラス化して、破損を防ぐことができます。

MyNavigationController:UINavigationController

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [super pushViewController:viewController animated:animated];
    self.interactivePopGestureRecognizer.enabled = NO; // disable
}

MyViewController:UIViewController

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
}

問題:煩わしすぎる

  • MyNavigationControllerMyViewControllerの代わりに、UINavigationControllerUIViewControllerを使用する必要があります。
  • UITableViewControllerUICollectionViewControllerなどのサブクラス化が必要です。

より良い解決策:メソッドスウィズリング

これは、UINavigationControllerメソッドとUIViewControllerメソッドをスウィズリングすることで実行できます。メソッドのスウィズリングについて知りたい場合は、 ここ にアクセスしてください。

以下の例では、メソッドのスウィズリングを簡単にする JRSwizzle を使用しています。

UINavigationController

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jr_swizzleMethod:@selector(viewDidLoad)
                    withMethod:@selector(hack_viewDidLoad)
                         error:nil];
        [self jr_swizzleMethod:@selector(pushViewController:animated:)
                    withMethod:@selector(hack_pushViewController:animated:)
                         error:nil];
    });
}

- (void)hack_viewDidLoad
{
    [self hack_viewDidLoad];
    self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
}

- (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [self hack_pushViewController:viewController animated:animated];
    self.interactivePopGestureRecognizer.enabled = NO;
}

UIViewController

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jr_swizzleMethod:@selector(viewDidAppear:)
                    withMethod:@selector(hack_viewDidAppear:)
                         error:nil];
    });
}

- (void)hack_viewDidAppear:(BOOL)animated
{
    [self hack_viewDidAppear:animated];
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}

シンプルであること:オープンソースを使用する

スワイプバック

SwipeBackコードなしで自動的に実行します。

CocoaPods の場合、Podfileに以下の行を追加するだけです。コードを書く必要はありません。 CocoaPodsは、SwipeBackをグローバルに自動的にインポートします。

pod 'SwipeBack'

ポッドをインストールすれば完了です!

0
devxoul