iOS present一個viewController后動畫失效的問題

問題描述

今天遇到一個基本問題,那就是:假設A為rootViewController,在適當的時機使用A present 另一個viewController B。然后會發現寫在B viewDidLoad中的動畫不會執行。

解決辦法

  • 方法一:將動畫寫在viewWillAppear或者之后執行的方法中,但是需要注意該方法會多次執行。
  • 方法二:將動畫的removedOnCompletion屬性設置為NO。

類似問題(切換后臺導致動畫失效)

在點擊home鍵切換到后臺,然后在切換回來時,會發現動畫都無效了。解決辦法與上面的類似:

  • 方法一:設置觀察者在app將要進入前臺時重置動畫。
  • 方法二:將動畫的removedOnCompletion屬性設置為NO。

總結

相信在開發中大多數人會遇到描述的第二個問題;而遇到present后動畫失效這個問題的人應該要少一些,因此搜索引擎上也就十分罕見,所以我把它記錄在此。至此問題就解決了,下面將要寫的是我是如何定位到這個問題的,如果您的時間寶貴,那么可以略過下面的篇幅。


實際遇到的問題和排查過程

問題描述

上面描述到的問題是我最后簡略所總結出的。實際的問題是:同事在一個view的初始化過程中添加了一個動畫(CoreAnimation),而這個動畫不執行了。

分析過程

  • 這個時候大家的第一反應肯定是動畫是不是寫的有問題?于是我在原來加動畫的地方寫出了另一個動畫(確定正確的)然后 command + R 發現然并卵。ps:同事也是試驗過其他動畫的。
  • 接著當然就是短暫的懵逼咯,仔細回想是不是忘記了什么,畢竟很久沒有寫動畫相關的代碼了,但是怎么想都想不出是為什么。
  • 接下來想到是不是viewController哪里出了問題,于是找到了viewController的實現代碼看了看,嗯,沒什么問題。又看了初始化的時候,簡單的alloc、init似乎也沒什么問題。
  • 那么問題出在哪里呢?然后我注意到了這個viewController是present出去的,難道說是這個原因?恰巧這是一個可以Push的視圖控制器(擁有navigationController),于是改用push方法,發現添加的動畫可以執行了!
  • 但是我還是不知道到底是為什么present的vc不執行動畫啊,接下來我直接將一個新創建的viewController(無多余的代碼),設置為rootViewController,在這個rootViewController中present出上面描述的含有動畫的視圖控制器,發現動畫還是不執行。
  • 我又重新創建了一個視圖控制器,在viewDidLoad中加入了相關的動畫,然后用rootViewController present它,接著運行工程,發現還是不執行。
  • 為了徹底排除是工程中一些配置所引起的原因,只有重新起一個工程來測試,然而在新的工程中還是不執行,那么至此可以斷定,是present導致的動畫不執行。
  • 問題找出來了,那么為什么?突然我想到,以前在做一個持續性動畫的時候,發現切換到后臺在切換回來后動畫就不起作用了,最后經過搜索和驗證發現是切換到后臺時所有的動畫被移除了,那么會不會present有類似的手法移除了動畫呢?那怎么驗證? 簡單的方式就是用dispatch_after。在設置延遲之后,發現動畫執行了。下面是代碼驗證過程:
 - (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor grayColor];
    
    v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
    v.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:v];
    
    CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    ani.fromValue = @0;
    ani.delegate = self;
    ani.toValue = @M_PI;
    ani.duration = 2.0;
    ani.repeatCount = HUGE_VALF;
    [v.layer addAnimation:ani forKey:@"opm"];
    NSLog(@"%@",v);

    
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%s  %@",__func__,v);
}

上述代碼中,在添加了動畫之后打印了一次v,然后viewWillAppear中也打印了一次v(目的是看看有沒有動畫在其中),控制臺的輸出為:

del07-10[8293:207637] <UIView: 0x7f836fc0b710; frame = (0 20; 100 100); animations = { opm=<CABasicAnimation: 0x6000000340e0>; }; layer = <CALayer: 0x600000034240>>
del07-10[8293:207637] -[PViewController viewWillAppear:]  <UIView: 0x7f836fc0b710; frame = (0 20; 100 100); layer = <CALayer: 0x600000034240>>

可以清楚的看到,在不加延遲時動畫在viewWillAppear中就已經不存在了。
當為加動畫的代碼塊設置延遲后,動畫就正常執行了。

  • 我就有點好奇,他是用什么方法移除所有的動畫的,不會是用layer的removeAllAnimations方法吧?雖然自己都不相信蘋果會用會用這個方法,但是好奇心太強,想試試,我想到兩個試的辦法:
    一、寫一個Layer繼承自CALayer,然后把相關layer都換成這個類的。
    二、Method Swizzling,利用runtime交換removeAllAnimations的實現。
    自然而然地,我采用了第二個方法,因為它有逼格啊 O(∩_∩)O
    為了保證一開始方法就被替換,我在appDelegate中加入了如下代碼:
AppDelegate.m
- (void)exchangeMethod {
    Method original = class_getInstanceMethod([CALayer class], @selector(removeAllAnimations));
    Method custom = class_getInstanceMethod([AppDelegate class], @selector(customMethod));
    method_exchangeImplementations(original, custom);
}

- (void)customMethod {
    NSLog(@"%s開始",__func__);
    NSLog(@"self=%@",self);
    NSLog(@"%s結束",__func__);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [self exchangeMethod];
    return YES;
}

經過以上代碼,我期待,在present的時候會打印出出我想要的東西,事實就是并沒有打印,既然一開始就料到了蘋果不會采用這么“低端”的方法來移除動畫,所以也沒什么好傷心的。寫都寫了,不如試試切換后臺呢?
我把Present 到的那個viewController做了下如修改(經過一段延遲之后再加動畫),使動畫能夠運行:

//關鍵代碼
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
        v.backgroundColor = [UIColor orangeColor];
        [self.view addSubview:v];
        
        CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        ani.fromValue = @0;
        ani.delegate = self;
        ani.toValue = @M_PI;
        ani.duration = 2.0;
        ani.repeatCount = HUGE_VALF;
        [v.layer addAnimation:ani forKey:@"opm"];
    });

等待動畫執行后,我把app切換到后臺,控制臺輸出:

del07-10[9174:251348] -[AppDelegate customMethod]開始
del07-10[9174:251348] self=<CALayer: 0x600000031280>
del07-10[9174:251348] -[AppDelegate customMethod]結束

很令人激動啊,居然執行了,也就是說調用了removeAllAnimations方法,如果有童鞋不理解為什么這里輸出的self是CALayer而不是Appdelegate,那么你們理解runtime的消息發送機制后應當會理解,這里就不多說了。

  • 既然證明調用了,那么就可以說,切換后臺和prensent他們清楚動畫的方式不一致。
    仔細推敲會發現,上面那句話是錯的,因為此時此刻我們并不知道這個Layer是屬于誰的。然后我嘗試打印出可見view以及superview中的layer,想要看看是哪個layer調用了移除動畫的方法。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        v = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
        v.backgroundColor = [UIColor orangeColor];
        [self.view addSubview:v];
        
        CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        ani.fromValue = @0;
        ani.delegate = self;
        ani.toValue = @M_PI;
        ani.duration = 2.0;
        ani.repeatCount = HUGE_VALF;
        [v.layer addAnimation:ani forKey:@"opm"];
        
        UIView *x = v;
        while (x) {
            NSLog(@"Layer尋找:%@",x.layer);
            x = x.superview;
        }
    });

重新運行,等待動畫執行,切到后臺。控制臺輸出如下:

del07-10[9362:256937] Layer尋找:<CALayer: 0x60000003ac80>
del07-10[9362:256937] Layer尋找:<CALayer: 0x60800003d240>
del07-10[9362:256937] Layer尋找:<CALayer: 0x60800003fde0>
del07-10[9362:256937] Layer尋找:<UIWindowLayer: 0x60800003cb80>
del07-10[9362:256937] -[AppDelegate customMethod]開始
del07-10[9362:256937] self=<CALayer: 0x60800022d240>
del07-10[9362:256937] -[AppDelegate customMethod]結束

經過多次的試驗,發現沒能找到與調用removeAllAnimations layer相同的layer。很失望啊!
然后各種折騰,最后靜下心來,在斷點調試中看到了如下信息:

<CALayer:0x60000003ae40; position = CGPoint (0 0); bounds = CGRect (0 0; 0 0); delegate = <UIKeyboardImpl: 0x7f84686238e0; frame = (0 0; 0 0); layer = <CALayer: 0x60000003ae40>>; opaque = YES; allowsGroupOpacity = YES; >

也就是說這個調用removeAllAnimations的layer可能是和鍵盤的動畫有關,雖然我不知道UIKeyboardImpl是個什么東東。

  • 然后我才想起用真機跑一下呢,什么代碼都不加,真機里面,切換到后臺,沒有任何一個layer調用removeAllAnimations。
  • 接著,我在界面上增加了一個UITextField,運行后點擊UITextField讓鍵盤彈出,然后切換到后臺。
    控制臺輸出如下:
del07-10[1911:634198] Layer尋找:<CALayer: 0x17403b500>
del07-10[1911:634198] Layer尋找:<CALayer: 0x17402d340>
del07-10[1911:634198] Layer尋找:<CALayer: 0x17002f900>
del07-10[1911:634198] Layer尋找:<UIWindowLayer: 0x17002ec40>
del07-10[1911:634198] -[AppDelegate customMethod]開始
del07-10[1911:634198] self=<CALayer: 0x17403df20>
del07-10[1911:634198] -[AppDelegate customMethod]結束
del07-10[1911:634198] -[AppDelegate customMethod]開始
del07-10[1911:634198] self=<CALayer: 0x17403dfc0>
del07-10[1911:634198] -[AppDelegate customMethod]結束
del07-10[1911:634198] -[AppDelegate customMethod]開始
del07-10[1911:634198] self=<CALayer: 0x17403df40>
del07-10[1911:634198] -[AppDelegate customMethod]結束
...//若干次

也就是說我們試驗到的調用removeAllAnimations的layer僅僅是和鍵盤相關的。
ps:在嘗試的過程中,也懷疑過是否是present的轉場動畫會與我們寫的沖突,所以系統才移除,嘗試時不僅僅將animated設置為了NO,還使用了自定義的轉場動畫,發現都是不能執行的。

綜述

經過這么一番折騰,我們可以知道的是當present一個viewController時,系統會移除該viewController的動畫;當切換到后臺是,系統也會移除當前動畫。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,255評論 4 61
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,573評論 6 30
  • 昨天宿舍發現老鼠,我不禁想起了我家的貓。 想起我家的貓,心里有些失落。在我們家搬到縣城后,每次回去,貓就很親熱的圍...
    gardendong閱讀 224評論 0 1
  • 有一種搞笑是我的樂觀感染了你,有一種搞笑是我的丑陋給你嘲笑
    David_Yum閱讀 117評論 0 0
  • 智障少女的自虐之旅 這是一個被稱為“超燃脂室外跑步”的訓練 4分鐘慢跑,400米快跑,200米慢跑,400米快跑,...
    流星雨lxy閱讀 1,826評論 1 0