蘋果最近發(fā)布 iOS14 正式版本瘾带,作為 iOS 開發(fā)者鼠哥,必須要進(jìn)行一波適配了。網(wǎng)上關(guān)于 iOS14 適配的文章已經(jīng)有挺多的了看政,通過這些文章我們就可以解決大多數(shù)的問題了朴恳,比如說:定位、相冊(cè)允蚣、UITableViewCell這些問題于颖。本文就不對(duì)這些問題贅述了,大家百度一下就解決了嚷兔。
本文就記錄一下森渐,我在更新 Xcode 之后,項(xiàng)目運(yùn)行在 iOS14 上的一些問題冒晰。主要就是下面的三點(diǎn)問題:
1同衣、Xcode 12 在模擬器上運(yùn)行項(xiàng)目時(shí)報(bào)錯(cuò)。
2壶运、使用 UIImageView 的 layer 做遮罩時(shí)乳怎,不顯示任何東西。
3、FLAnimatedImage 播放單次循環(huán)GIF蚪缀,跳轉(zhuǎn)新頁面回來之后,就不顯示東西了恕出。
1询枚、Xcode 12 在模擬器上運(yùn)行項(xiàng)目時(shí)報(bào)錯(cuò)
當(dāng)我更新完 Xcode 迫不及待的想看一下項(xiàng)目在 iOS14 上是否可以完美運(yùn)行時(shí),就得到了下面的提示浙巫。
No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=x86_64, VALID_ARCHS=).
看到的第一眼就是金蜀,這問題難不倒我哦,添加一下對(duì) x86_64 的支持就好了的畴。網(wǎng)上相關(guān)文章也是特別多渊抄,不了解 "i386 x86_64 arm64 armv7 armv7s" 這一堆玩意兒的可以自己百度一下哦。
然后我就開始操作:
project targets -> build settings -> architectures -> VALID_ARCHS
當(dāng)我操作到第三步的時(shí)候丧裁,我就傻了护桦,Xcode12 項(xiàng)目配置里找不到 VALID_ARCHS。替代的是 Excluded Architectures煎娇。啥二庵、啥、啥缓呛、這都是啥啊催享。
這TMD不是我熟悉的 Xcode,內(nèi)心一萬頭曹尼瑪奔騰過后哟绊,理智告訴自己問題還是要解決的因妙。經(jīng)過一堆百度、Google 終于找到解決方案票髓。
project targets -> build settings -> user-defined
在 build settings 里攀涵,有個(gè) user-defined 這個(gè)玩意兒,VALID_ARCHS 現(xiàn)在在這里設(shè)置一下就OK了炬称。有的朋友可能又會(huì)問了汁果,啊啊啊,我在 user-defined 里也沒有找到 VALID_ARCHS 啊玲躯,這時(shí)候你會(huì)看到 Xcode 界面頂上有一個(gè) + 据德,點(diǎn)擊一下,就會(huì)恍然大悟跷车。
2棘利、使用 UIImageView 的 layer 做遮罩時(shí),不顯示任何東西朽缴。
項(xiàng)目終于運(yùn)行起來善玫,點(diǎn)點(diǎn)看看吧,點(diǎn)著點(diǎn)著 bug 就出現(xiàn)了密强。
本來應(yīng)該出現(xiàn)上圖效果的情況下茅郎,在 iOS14 上竟然空白一片蜗元,最騷包的是對(duì)點(diǎn)擊事件竟然沒有影響。研究一下:
上面這種效果應(yīng)該 iOS 開發(fā)老司機(jī)應(yīng)該都知道怎么搞的系冗,就是通過 layer.mask 來實(shí)現(xiàn)的奕扣。具體來說可以通過下面兩種方式:
1、通過 UIBezierPath 畫出氣泡的樣式來做遮罩掌敬。
// 繪制氣泡惯豆,得到對(duì)應(yīng)的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];
//設(shè)置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];
//設(shè)置layer.mask
imageView.layer.mask = maskImageView.layer
我這邊用的是第二種方式楷兽,通過把汽包圖片拉伸到合適的尺寸之后,對(duì)展示的圖片做遮罩實(shí)現(xiàn)的华临。對(duì) layer.mask 不了解的同學(xué)芯杀,可以自己百度一下哈。
看到這個(gè)現(xiàn)象银舱,首先想到的就是 maskImageView.layer 除了問題瘪匿,通過斷點(diǎn)我發(fā)現(xiàn)下面的問題。
通過對(duì)比可以發(fā)現(xiàn)棋弥,layer 中少了 contents 的信息。這點(diǎn)不知道是bug诚欠,還是官方特意為之顽染。畢竟咱也沒看相關(guān)的文檔。
這樣的話轰绵,就不能用 UIImageView 的 layer 啦粉寞, 要自己搞出一個(gè) layer 了。首先想到的就是通過 UIBezierPath 來繪制左腔,通過這種方案來實(shí)現(xiàn)完全是可行的唧垦,但是這個(gè)氣泡里面的圓角、拐點(diǎn)什么的太多了液样,搞起來就太麻煩振亮,放棄嘍。鞭莽。坊秸。
想其它辦法,只要肯動(dòng)腦辦法總比困難多澎怒。通過查找資料褒搔,發(fā)現(xiàn) layer 可以設(shè)置 contents,而這個(gè)玩意兒可以接收 UIImage 的對(duì)象。開搞:
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = imageView.bounds;
[maskLayer setContents:(id)[[UIImage imageNamed:@"SenderTextNodeBkg"] stretchableImageWithLeftCapWidth:50 topCapHeight:30].CGImage];
imageView.layer.mask = maskLayer;
通過上面代碼運(yùn)行出來的結(jié)果是這樣的:
由此看來星瘾,氣泡圖片的拉伸出了一點(diǎn)點(diǎn)的問題哦走孽,看來拉伸圖片的方法只有把圖片放在 UIImageView 上才好使哦,那我們就來自己弄個(gè)適合尺寸的圖片吧琳状。
/// 對(duì)當(dāng)前圖片拉伸到對(duì)應(yīng)尺寸的圖片
/// @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;
通過一頓操作融求,女神回來了,我也安心了算撮。
3、FLAnimatedImage 播放單次循環(huán)GIF县昂,跳轉(zhuǎn)新頁面回來之后肮柜,就不顯示東西了。
今天在看自己的APP倒彰,發(fā)現(xiàn)一個(gè)播放GIF的運(yùn)營位置审洞,是白白一塊兒,頁面刷新之后待讳,就開始播放GIF芒澜,但是播放一次就停止了,最詭異的是创淡,點(diǎn)擊進(jìn)入二級(jí)頁面再返回的時(shí)候痴晦,這一塊就啥也不顯示了,但是不影響點(diǎn)擊事件琳彩。
看不懂描述誊酌,上圖。
我們影響中播放GIF是這樣的露乏,不停的循環(huán)碧浊,就算點(diǎn)擊到二級(jí)頁面回來也是在循環(huán)的。
但是今天我在 iOS14 上發(fā)現(xiàn)是這樣的瘟仿,就是 gif 只播放了一次就暫停了箱锐,點(diǎn)擊跳轉(zhuǎn)二級(jí)頁面之后回來就啥沒了,我的羅老師呢劳较?
經(jīng)過我查找資料和實(shí)驗(yàn)得出以下幾點(diǎn):
1驹止、GIF 在中做出來的時(shí)候是可以選擇循環(huán)次數(shù)的,而羅老師這個(gè) gif 的循環(huán)次數(shù)設(shè)置的就是1次兴想。
2幢哨、在 FLAnimatedImage 類中,會(huì)通過下面的方法獲取循環(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践樱、當(dāng)我把循環(huán)次數(shù)寫死為1的時(shí)候,不論哪個(gè)版本的系統(tǒng)都會(huì)出現(xiàn)上述問題凸丸。
我們先不考慮為啥之前獲取循環(huán)次數(shù)不對(duì)拷邢,就探索一下為啥單次循環(huán)時(shí),會(huì)出現(xiàn) UIImageView 不顯示內(nèi)容的問題屎慢。
通過觀察 FLAnimatedImageView 源碼可以發(fā)現(xiàn)瞭稼。
// 當(dāng)我們?cè)O(shè)置動(dòng)圖的時(shí)候會(huì)執(zhí)行這個(gè)方法。
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
/*
省略的代碼
*/
if (animatedImage) {
// 當(dāng)我們?cè)O(shè)置動(dòng)圖的時(shí)候腻惠,會(huì)把 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 動(dòng)畫的實(shí)現(xiàn)追蹤依賴這個(gè)方法
- (void)displayDidRefresh:(CADisplayLink *)displayLink{
/*
省略的代碼
*/
//取出對(duì)應(yīng)幀的圖片
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
//記錄當(dāng)前幀的圖片
self.currentFrame = image;
if (self.needsDisplayWhenImageBecomesAvailable) {
//通過不停的重繪layer环肘,實(shí)現(xiàn) GIF 效果。會(huì)觸發(fā) displayLayer 這代理方法集灌。
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
/*
省略的代碼
*/
}
//繪制layer
- (void)displayLayer:(CALayer *)layer
{
//取出當(dāng)前圖片悔雹,繪制 layer.contents
layer.contents = (__bridge id)self.image.CGImage;
}
// 重寫 image getter 方法
- (UIImage *)image
{
UIImage *image = nil;
if (self.animatedImage) {
//如果是動(dòng)圖,就返回當(dāng)前幀圖片
image = self.currentFrame;
} else {
//不然就是 imageview 的 image欣喧。
image = super.image;
}
return image;
}
上面這一堆邏輯看起來都是沒有問題的腌零,在 gif 為無限循環(huán)時(shí),也是可以完美運(yù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),當(dāng) loopCountdown == 0 的時(shí)候酷鸦,我們的 GIF 就會(huì)停掉饰躲。
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;
}
/*
省略的代碼
*/
}
看上去上面對(duì) gif 停止的操作也是沒有問題的,GIF 停止時(shí)所展示的畫面是 GIF 最后一幀的圖片臼隔,所展示的內(nèi)容是通過 layer.contents 繪制出來的嘹裂。
但是事實(shí)告訴我們當(dāng)頁面發(fā)生跳轉(zhuǎn)時(shí),內(nèi)容就消失掉了摔握,具體為啥我這邊也沒有查到相關(guān)的資料寄狼。當(dāng)時(shí)我們可以自己實(shí)驗(yàn):
當(dāng)我寫下下面代碼的時(shí)候,我的預(yù)想的結(jié)果應(yīng)該是會(huì)顯示圖片的
let imageView:FLAnimatedImageView = FLAnimatedImageView.init()
self.view.addSubview(imageView)
imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage
但事實(shí)是毛都沒有氨淌,為啥呢泊愧?經(jīng)過我的研究發(fā)現(xiàn)這么一個(gè)問題,就是當(dāng)我們?cè)O(shè)置過 layer.contents 之后盛正,還會(huì)執(zhí)行下面一堆方法:
這一堆方法删咱,最終會(huì)調(diào)到 UIImageView image 的 getter 方法,而這個(gè)時(shí)候 image 就是 nil豪筝,然后 UIImageView 沒有東西就解釋的過去了痰滋。
當(dāng)我們?cè)O(shè)置 layer.contents 時(shí)做個(gè)延遲摘能,果然圖片就顯示了。
dispense_after({
imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage
}, 5)
按照這個(gè)思路敲街,我以為頁面跳轉(zhuǎn)的時(shí)候也是因?yàn)?image 被賦值為 nil 而導(dǎo)致的內(nèi)容消失的团搞,可事實(shí)并不是,因?yàn)?UIImageView image 的 getter 方法并沒有執(zhí)行多艇。
我發(fā)現(xiàn)在頁面 push 時(shí)逻恐,didMoveToWindow 會(huì)執(zhí)行3次,再 pop 時(shí)峻黍,又會(huì)執(zhí)行一次复隆。對(duì)于 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.
大概意思是窗口變化就會(huì)執(zhí)行昏名,其實(shí)我沒太明白,查了資料也啥都沒有查到阵面,在 didMoveToWindow 執(zhí)行第二次的時(shí)候,打印 layer.contents 發(fā)現(xiàn)這個(gè)玩意兒沒東西了洪鸭。具體在哪里被干掉了也沒整明白样刷。。览爵。
當(dāng)我把 UIImageView 換成 UIView 時(shí)置鼻,就不存在跳轉(zhuǎn)時(shí) layer.contents 被置空的問題了。應(yīng)該是 UIImageView 內(nèi)部對(duì) layer 又搞了什么玩意兒蜓竹,沒有源碼太難了箕母。所以到最后也沒整明白為啥。俱济。嘶是。
知道問題所在我們就可以有解決方案了,方案不止一種蛛碌,我用的是下面的方法:
在動(dòng)畫結(jié)束的時(shí)候聂喇,重新給 image 賦值。
if (self.loopCountdown == 0) {
super.image = image;
[self stopAnimating];
return;
}
我自己又做了一些探究蔚携,發(fā)現(xiàn)了另外一個(gè)問題:
//當(dāng)頁面跳轉(zhuǎn)發(fā)生跳轉(zhuǎn)時(shí)希太,會(huì)執(zhí)行這個(gè)方法
- (void)didMoveToWindow{
[super didMoveToWindow];
// 更新是否需要?jiǎng)赢嫷姆椒ǎ? [self updateShouldAnimate];
// 即使GIF已經(jīng)到達(dá)最大次數(shù)而停止, self.shouldAnimate 該字段仍然時(shí) ture酝蜒,因?yàn)樗蹬c循環(huán)次數(shù)沒有關(guān)系誊辉。這樣就導(dǎo)致 displayDidRefresh
if (self.shouldAnimate) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
//更新是否需要?jiǎng)赢嫷姆椒?- (void)updateShouldAnimate
{
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
self.shouldAnimate = self.animatedImage && isVisible;
}
本文就結(jié)束了,后續(xù)遇到其他奇怪問題在補(bǔ)充亡脑。