iOS 14適配(模擬器運行報錯;UIImageView 做遮罩;FLAnimatedImage GIF播放)

蘋果最近發(fā)布 iOS14 正式版本,作為 iOS 開發(fā)者,必須要進行一波適配了。網(wǎng)上關于 iOS14 適配的文章已經(jīng)有挺多的了,通過這些文章我們就可以解決大多數(shù)的問題了,比如說:定位、相冊、UITableViewCell這些問題。本文就不對這些問題贅述了,大家百度一下就解決了。

本文就記錄一下,我在更新 Xcode 之后,項目運行在 iOS14 上的一些問題。主要就是下面的三點問題:
1、Xcode 12 在模擬器上運行項目時報錯。
2、使用 UIImageView 的 layer 做遮罩時,不顯示任何東西。
3、FLAnimatedImage 播放單次循環(huán)GIF,跳轉(zhuǎn)新頁面回來之后,就不顯示東西了。

1、Xcode 12 在模擬器上運行項目時報錯

當我更新完 Xcode 迫不及待的想看一下項目在 iOS14 上是否可以完美運行時,就得到了下面的提示。

No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=x86_64, VALID_ARCHS=).

模擬器運行報錯.png

看到的第一眼就是,這問題難不倒我哦,添加一下對 x86_64 的支持就好了。網(wǎng)上相關文章也是特別多,不了解 "i386 x86_64 arm64 armv7 armv7s" 這一堆玩意兒的可以自己百度一下哦。

然后我就開始操作:
project targets -> build settings -> architectures -> VALID_ARCHS

當我操作到第三步的時候,我就傻了,Xcode12 項目配置里找不到 VALID_ARCHS。替代的是 Excluded Architectures。啥、啥、啥、這都是啥啊。

Architectures.png

這TMD不是我熟悉的 Xcode,內(nèi)心一萬頭曹尼瑪奔騰過后,理智告訴自己問題還是要解決的。經(jīng)過一堆百度、Google 終于找到解決方案。

project targets -> build settings -> user-defined

在 build settings 里,有個 user-defined 這個玩意兒,VALID_ARCHS 現(xiàn)在在這里設置一下就OK了。有的朋友可能又會問了,啊啊啊,我在 user-defined 里也沒有找到 VALID_ARCHS 啊,這時候你會看到 Xcode 界面頂上有一個 + ,點擊一下,就會恍然大悟。

2、使用 UIImageView 的 layer 做遮罩時,不顯示任何東西。

項目終于運行起來,點點看看吧,點著點著 bug 就出現(xiàn)了。

iOS13.4 效果圖.png

本來應該出現(xiàn)上圖效果的情況下,在 iOS14 上竟然空白一片,最騷包的是對點擊事件竟然沒有影響。研究一下:
上面這種效果應該 iOS 開發(fā)老司機應該都知道怎么搞的,就是通過 layer.mask 來實現(xiàn)的。具體來說可以通過下面兩種方式:

1、通過 UIBezierPath 畫出氣泡的樣式來做遮罩。

// 繪制氣泡,得到對應的layer
UIBezierPath *path = [UIBezierPath bezierPath];
/*
   繪制氣泡的代碼
*/
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;

//要展示的圖片
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
[imageView setImage:[UIImage imageNamed:@"lock_wallpaper.jpg"]];
[self.view addSubview:imageView];

//設置layer.mask
imageView.layer.mask = layer

2、通過 UIImageView 的 layer 做做遮罩。

//做 mask 的圖片
UIImageView *maskImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
maskImageView.image = [[UIImage imageNamed:@"SenderTextNodeBkg"] stretchableImageWithLeftCapWidth:50 topCapHeight:30];//遮蓋圖片

//要展示的圖片
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
[imageView setImage:[UIImage imageNamed:@"lock_wallpaper.jpg"]];
[self.view addSubview:imageView];

//設置layer.mask
imageView.layer.mask = maskImageView.layer
SenderTextNodeBkg@3x.png

我這邊用的是第二種方式,通過把汽包圖片拉伸到合適的尺寸之后,對展示的圖片做遮罩實現(xiàn)的。對 layer.mask 不了解的同學,可以自己百度一下哈。

看到這個現(xiàn)象,首先想到的就是 maskImageView.layer 除了問題,通過斷點我發(fā)現(xiàn)下面的問題。

iOS14 之前的layer.png
iOS14 之后的layer.png

通過對比可以發(fā)現(xiàn),layer 中少了 contents 的信息。這點不知道是bug,還是官方特意為之。畢竟咱也沒看相關的文檔。

這樣的話,就不能用 UIImageView 的 layer 啦, 要自己搞出一個 layer 了。首先想到的就是通過 UIBezierPath 來繪制,通過這種方案來實現(xiàn)完全是可行的,但是這個氣泡里面的圓角、拐點什么的太多了,搞起來就太麻煩,放棄嘍。。。
想其它辦法,只要肯動腦辦法總比困難多。通過查找資料,發(fā)現(xiàn) layer 可以設置 contents,而這個玩意兒可以接收 UIImage 的對象。開搞:

CALayer *maskLayer = [CALayer layer];
maskLayer.frame = imageView.bounds;
[maskLayer setContents:(id)[[UIImage imageNamed:@"SenderTextNodeBkg"] stretchableImageWithLeftCapWidth:50 topCapHeight:30].CGImage];

imageView.layer.mask = maskLayer;

通過上面代碼運行出來的結(jié)果是這樣的:


結(jié)果1.png

由此看來,氣泡圖片的拉伸出了一點點的問題哦,看來拉伸圖片的方法只有把圖片放在 UIImageView 上才好使哦,那我們就來自己弄個適合尺寸的圖片吧。

/// 對當前圖片拉伸到對應尺寸的圖片
/// @param originImage 原始圖片
/// @param newSize 新的尺寸
/// @param leftCapWidth
/// @param topCapHeight 
- (UIImage *)stretchImageWithOriginImage:(UIImage *)originImage newSize:(CGSize)newSize leftCapWidth:(CGFloat)leftCapWidth topCapHeight:(CGFloat)topCapHeight{
    UIImage *newImage;
    newImage = [originImage stretchableImageWithLeftCapWidth:leftCapWidth topCapHeight:topCapHeight];
    UIGraphicsBeginImageContextWithOptions(newSize, false, 0);
    [newImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
    
    newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}


CALayer *maskLayer = [CALayer layer];
maskLayer.frame = imageView.bounds;
[maskLayer setContents:(id)[self stretchImageWithOriginImage:[UIImage imageNamed:@"SenderTextNodeBkg"] newSize:CGSizeMake(200, 355.2) leftCapWidth:50 topCapHeight:30].CGImage];

imageView.layer.mask = maskLayer;
結(jié)果2.png

通過一頓操作,女神回來了,我也安心了。

3、FLAnimatedImage 播放單次循環(huán)GIF,跳轉(zhuǎn)新頁面回來之后,就不顯示東西了。

今天在看自己的APP,發(fā)現(xiàn)一個播放GIF的運營位置,是白白一塊兒,頁面刷新之后,就開始播放GIF,但是播放一次就停止了,最詭異的是,點擊進入二級頁面再返回的時候,這一塊就啥也不顯示了,但是不影響點擊事件。

看不懂描述,上圖。

我們影響中播放GIF是這樣的,不停的循環(huán),就算點擊到二級頁面回來也是在循環(huán)的。

羅老師.gif

但是今天我在 iOS14 上發(fā)現(xiàn)是這樣的,就是 gif 只播放了一次就暫停了,點擊跳轉(zhuǎn)二級頁面之后回來就啥沒了,我的羅老師呢?

羅老師.gif

經(jīng)過我查找資料和實驗得出以下幾點:
1、GIF 在中做出來的時候是可以選擇循環(huán)次數(shù)的,而羅老師這個 gif 的循環(huán)次數(shù)設置的就是1次。
2、在 FLAnimatedImage 類中,會通過下面的方法獲取循環(huán)次數(shù)。

 _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];

有意思的是同樣一張 GIF,iOS14 系統(tǒng)獲取的次數(shù)是1,而之前的系統(tǒng)獲取的次數(shù)是0,也就是無線循環(huán)。
3、當我把循環(huán)次數(shù)寫死為1的時候,不論哪個版本的系統(tǒng)都會出現(xiàn)上述問題。

我們先不考慮為啥之前獲取循環(huán)次數(shù)不對,就探索一下為啥單次循環(huán)時,會出現(xiàn) UIImageView 不顯示內(nèi)容的問題。

通過觀察 FLAnimatedImageView 源碼可以發(fā)現(xiàn)。

// 當我們設置動圖的時候會執(zhí)行這個方法。
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
/*
    省略的代碼
*/
       if (animatedImage) {
            // 當我們設置動圖的時候,會把 ImageView 的 image 置為 nil
            super.image = nil;
            // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
            super.highlighted = NO;
            // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
            [self invalidateIntrinsicContentSize];
        } else {
            // Stop animating before the animated image gets cleared out.
            [self stopAnimating];
        }

/*
    省略的代碼
*/
}

// gif 動畫的實現(xiàn)追蹤依賴這個方法
- (void)displayDidRefresh:(CADisplayLink *)displayLink{
/*
    省略的代碼
*/
        //取出對應幀的圖片
        UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
        if (image) {
            FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
           //記錄當前幀的圖片
            self.currentFrame = image;
            if (self.needsDisplayWhenImageBecomesAvailable) {
                //通過不停的重繪layer,實現(xiàn) GIF 效果。會觸發(fā) displayLayer 這代理方法。
                [self.layer setNeedsDisplay];
                self.needsDisplayWhenImageBecomesAvailable = NO;
            }
/*
    省略的代碼
*/
}

//繪制layer
- (void)displayLayer:(CALayer *)layer
{
    //取出當前圖片,繪制 layer.contents
    layer.contents = (__bridge id)self.image.CGImage;
}

// 重寫 image getter 方法
- (UIImage *)image
{
    UIImage *image = nil;
    if (self.animatedImage) {
        //如果是動圖,就返回當前幀圖片
        image = self.currentFrame;
    } else {
        //不然就是 imageview 的 image。
        image = super.image;
    }
    return image;
}

上面這一堆邏輯看起來都是沒有問題的,在 gif 為無限循環(huán)時,也是可以完美運行的。那么就要看一下程序?qū)ρh(huán)次數(shù)固定的 gif 是怎么處理的吧。

- (void)displayDidRefresh:(CADisplayLink *)displayLink{
/*
    省略的代碼
*/
            while (self.accumulator >= delayTime) {
                self.accumulator -= delayTime;
                self.currentFrameIndex++;
                if (self.currentFrameIndex >= self.animatedImage.frameCount) {
                    // If we've looped the number of times that this animated image describes, stop looping.
                    self.loopCountdown--;
                    if (self.loopCompletionBlock) {
                        self.loopCompletionBlock(self.loopCountdown);
                    }
                    
                   //這里我們可以發(fā)現(xiàn),當 loopCountdown == 0 的時候,我們的 GIF 就會停掉。
                    if (self.loopCountdown == 0) {
                        [self stopAnimating];
                        return;
                    }
                    self.currentFrameIndex = 0;
                }
                // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
                // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
                self.needsDisplayWhenImageBecomesAvailable = YES;
            }
/*
    省略的代碼
*/
}

看上去上面對 gif 停止的操作也是沒有問題的,GIF 停止時所展示的畫面是 GIF 最后一幀的圖片,所展示的內(nèi)容是通過 layer.contents 繪制出來的。

但是事實告訴我們當頁面發(fā)生跳轉(zhuǎn)時,內(nèi)容就消失掉了,具體為啥我這邊也沒有查到相關的資料。當時我們可以自己實驗:

當我寫下下面代碼的時候,我的預想的結(jié)果應該是會顯示圖片的

let imageView:FLAnimatedImageView = FLAnimatedImageView.init()
self.view.addSubview(imageView)
imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage

但事實是毛都沒有,為啥呢?經(jīng)過我的研究發(fā)現(xiàn)這么一個問題,就是當我們設置過 layer.contents 之后,還會執(zhí)行下面一堆方法:

一堆方法.png

這一堆方法,最終會調(diào)到 UIImageView image 的 getter 方法,而這個時候 image 就是 nil,然后 UIImageView 沒有東西就解釋的過去了。

當我們設置 layer.contents 時做個延遲,果然圖片就顯示了。

 dispense_after({
      imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage
  }, 5)

按照這個思路,我以為頁面跳轉(zhuǎn)的時候也是因為 image 被賦值為 nil 而導致的內(nèi)容消失的,可事實并不是,因為 UIImageView image 的 getter 方法并沒有執(zhí)行。

我發(fā)現(xiàn)在頁面 push 時,didMoveToWindow 會執(zhí)行3次,再 pop 時,又會執(zhí)行一次。對于 didMoveToWindow 解釋,官方文檔是這樣的。


The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.

The [`window`](https://developer.apple.com/documentation/uikit/uiview/1622456-window) property may be `nil` by the time that this method is called, indicating that the receiver does not currently reside in any window. This occurs when the receiver has just been removed from its superview or when the receiver has just been added to a superview that is not attached to a window. Overrides of this method may choose to ignore such cases if they are not of interest.

大概意思是窗口變化就會執(zhí)行,其實我沒太明白,查了資料也啥都沒有查到,在 didMoveToWindow 執(zhí)行第二次的時候,打印 layer.contents 發(fā)現(xiàn)這個玩意兒沒東西了。具體在哪里被干掉了也沒整明白。。。

當我把 UIImageView 換成 UIView 時,就不存在跳轉(zhuǎn)時 layer.contents 被置空的問題了。應該是 UIImageView 內(nèi)部對 layer 又搞了什么玩意兒,沒有源碼太難了。所以到最后也沒整明白為啥。。。

知道問題所在我們就可以有解決方案了,方案不止一種,我用的是下面的方法:

在動畫結(jié)束的時候,重新給 image 賦值。
if (self.loopCountdown == 0) {
     super.image = image;
     [self stopAnimating];
     return;
}

我自己又做了一些探究,發(fā)現(xiàn)了另外一個問題:

//當頁面跳轉(zhuǎn)發(fā)生跳轉(zhuǎn)時,會執(zhí)行這個方法
- (void)didMoveToWindow{
    [super didMoveToWindow];
   // 更新是否需要動畫的方法,
    [self updateShouldAnimate];
    // 即使GIF已經(jīng)到達最大次數(shù)而停止, self.shouldAnimate 該字段仍然時 ture,因為它值與循環(huán)次數(shù)沒有關系。這樣就導致 displayDidRefresh 
    if (self.shouldAnimate) {
        [self startAnimating];
    } else {
        [self stopAnimating];
    }
}

//更新是否需要動畫的方法
- (void)updateShouldAnimate
{
    BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
    self.shouldAnimate = self.animatedImage && isVisible;
}

本文就結(jié)束了,后續(xù)遇到其他奇怪問題在補充。

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

推薦閱讀更多精彩內(nèi)容