IOS 屏幕適配(一)理論篇

@[TOC](IOS 屏幕適配(一)理論篇)

1. IOS 屏幕適配基本概念

1.1 IOS 設(shè)備的尺寸和分辨率

1.1.1 分辨率相關(guān)概念

  • 點(diǎn)(Points):

是iOS開發(fā)中引入的抽象單位,稱作點(diǎn)。開發(fā)過程中所有基于坐標(biāo)系的繪制都是以 point 作為單位,在iPhone 2G,3G,3GS的年代,point 和屏幕上的像素是完全一一對(duì)應(yīng)的,即 320 * 480 (points), 也是 320 * 480 (pixels)

  • 渲染像素 (Rendered Pixels):

Rendered Pixels: 渲染像素, 以 point 為單位的繪制最終都會(huì)渲染成 pixels,這個(gè)過程被稱為光柵化?;?point 的坐標(biāo)系乘以比例因子可以得到基于像素的坐標(biāo)系,高比例因子會(huì)使更多的細(xì)節(jié)展示,目前的比例因子會(huì)是 1x,2x,3x

  • 物理像素(Physical Pixels):

Physical Pixels: 物理像素,就是設(shè)備屏幕實(shí)際的像素

  • 設(shè)備屏幕的物理長(zhǎng)度(Physical Device):

Physical Device: 設(shè)備屏幕的物理長(zhǎng)度,使用英寸作為單位。比如iPhone 4屏幕是3.5英寸,iPhone 5 是4英寸,iphone 6是4.7英寸,這里的數(shù)字是指手機(jī)屏幕對(duì)角線的物理長(zhǎng)度。實(shí)際上會(huì)是Physical Pixels的像素值(而不是Rendered Pixels的像素值)會(huì)渲染到該屏幕上, 屏幕會(huì)有 PPI(pixels-per-inch) 的特性,PPI 的值告訴你每英寸會(huì)有多少像素渲染。

1.1.2 IOS 各個(gè)設(shè)備對(duì)應(yīng)的分辨率

IOS 各個(gè)設(shè)備對(duì)應(yīng)的分辨率
機(jī)型 屏幕寬高(point) 渲染像素(pixel) 物理像素(pixel) 屏幕對(duì)角線長(zhǎng)度(英寸) 屏幕模式
iPhone 2G,3G,3GS 320 * 480 320 * 480 320 * 480 3.5(163PPI) 1x
iPhone 4, 4s 320 * 480 640 * 960 640 * 960 3.5 (326PPI) 2x
iPhone 5, 5s 320 * 568 640 * 1136 640 * 1136 4 (326PPI) 2x
iPhone 6, 6s, 7 375 * 667 750 * 1334 750 * 1334 4.7 (326PPI) 2x
iPhone 6 Plus, 6s Plus, 7 Plus 414 * 736 1242 * 2208 1080 * 1920 5.5 (401PPI) 3x
  • iphone設(shè)備尺寸
機(jī)型 屏幕寬高(point) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iPhone 2g 480×320 3:2 163ppi 3.5 iPhone1,1 2008.01
iPhone 3g 480×320 3:2 163ppi 3.5 iPhone1,2 2008.06
iPhone 3gs 480×320 3:2 163ppi 3.5 iPhone2,1 2009.06
iPhone 4 960×640 3:2 163ppi 3.5 iPhone3,1、iPhone3,2、iPhone3,3 2010.06
iPhone 4s 960×640 3:2 326ppi 3.5 iPhone4,1 2011.10
iPhone 5 1136×640 16:9 326ppi 4.0 iPhone5,1、iPhone5,2 2012.09
iPhone 5c 1136×640 16:9 326ppi 4.0 iPhone5,3、iPhone5,4 2013.09
iPhone 5s 1136×640 16:9 326ppi 4.0 iPhone6,1、iPhone6,2 2013.09
iPhone 6 1334×750 16:9 401ppi 4.7 iPhone7,2 2014.09
iPhone 6 plus 1920×1080 16:9 401ppi 5.5 iPhone7,1 2014.09
iPhone 6s 1334×750 16:9 401ppi 4.7 iPhone8,2 2015.09
iPhone 6s plus 1920×1080 16:9 401ppi 5.5 iPhone8,1 2015.09
iPhone 5 SE 1136×640 16:9 401ppi 4.0 iPhone8,4 2016.03
iPhone 7 1334×750 16:9 401ppi 4.7 iPhone9,1、iPhone9,3 2016.09
iPhone 7 plus 1920×1080 16:9 401ppi 5.5 iPhone9,2、iPhone9,4 2016.09
iPhone 8 1334×750 16:9 401ppi 4.7 iPhone10,1、iPhone10,4 2017.09
iPhone 8 plus 1920×1080 16:9 401ppi 5.5 iPhone10,2、iPhone10,5 2017.09
iPhone X 2436×1125 18:9 458ppi 5.8 iPhone10,3、iPhone10,6 2017.09
iPhone XS 2436×1125 18:9 458ppi 5.8 iPhone11,2 2018.09
iPhone XS Max 2688×1242 18:9 458ppi 6.5 iPhone11,4、iPhone11,6 2018.09
iPhone XR 1792×828 19.5:9 326ppi 6.1 iPhone11,8 2018.09
  • ipad 尺寸
機(jī)型 屏幕寬高(point) 屏幕模式(Scale) 物理像素(pixel) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iPad 1024×768 @1x 1024×768 4:3 163ppi 9.7 iPad1,1 2010.01
iPad 2 1024×768 @1x 1024×768 4:3 163ppi 9.7 iPad2,1、iPad2,2、iPad2,3、iPad2,4 2011.03
iPad 3(New) 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad3,1、iPad3,2、iPad3,3 2012.03
iPad 4 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad3,4、iPad3,5、iPad3,6 2012.10
iPad 5 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad6,11、iPad6,12 2017.03
  • ipad Air尺寸
機(jī)型 屏幕寬高(point) 屏幕模式(Scale) 物理像素(pixel) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iPad Air 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad4,1、iPad4,2、iPad4,3 2013.10
iPad Air 2 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad5,3、iPad5,4 2014.10
  • iPad Pro 尺寸
機(jī)型 屏幕寬高(point) 屏幕模式(Scale) 物理像素(pixel) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iPad Pro 12.9-inch 1366×1024 @2x 2732×2048 4:3 264ppi 12.9 iPad6,7、iPad6,8 2015.09
iPad Pro 9.7-inch 1024×768 @2x 2048×1536 4:3 264ppi 9.7 iPad6,3、iPad6,4 2016.03
iPad Pro 12.9-inch 2 1366×1024 @2x 2732×2048 4:3 264ppi 12.9 iPad7,1、iPad7,2 2017
iPad Pro 10.5 1112×834 @2x 2224×1668 4:3 264ppi 10.5 iPad7,3、iPad7,4
  • ipad Mini尺寸
機(jī)型 屏幕寬高(point) 屏幕模式(Scale) 物理像素(pixel) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iPad mini 1024×768 @1X 1024×768 4:3 163 7.9 iPad2,5、iPad2,6、iPad2,7 2012.10
iPad mini 2 1024×768 @2X 2048×1536 4:3 326 7.9 iPad4,5、iPad4,6、iPad4,7 2013.10
iPad mini 3 1024×768 @2X 2048×1536 4:3 326 7.9 iPad4,7、iPad4,8、iPad4,9 2014.10
iPad mini 4 1024×768 @2X 2048×1536 4:3 326 7.9 iPad5,1、iPad5,2 2015.09
  • iPod Touch尺寸
機(jī)型 屏幕寬高(point) 屏幕模式(Scale) 物理像素(pixel) 比例 像素密度(PPI) 屏幕尺寸 型號(hào)代碼 發(fā)布日
iTouch 480*320 @1X 480*320 3:2 163ppi 3.5 iPod1,1 2007.09
iTouch 2 480*320 @1X 480*320 3:2 163ppi 3.5 iPod2,1 2008.09
iTouch 3 480*320 @1X 480*320 3:2 163ppi 3.5 iPod3,1 2009.09
iTouch 4 480*320 @2X 960*640 3:2 326ppi 3.5 iPod4,1 2010.09
iTouch 5 568*320 @2X 1136*640 16:9 326ppi 4.0 iPod5,1 2012.09
iTouch 6 568*320 @2X 1136*640 16:9 326ppi 4.0 iPod7,1 2015.07
  • 1x, 2x, 3x 的含義:

屏幕模式,描述的就是屏幕中一個(gè)點(diǎn)有多少個(gè) Rendered Pixels 渲染,對(duì)于2倍屏(又稱 Retina 顯示屏),會(huì)有 2 * 2 = 4 個(gè)像素的面積渲染,對(duì)于3倍屏(又稱 Retina HD 顯示屏),會(huì)有 3 * 3 = 9 個(gè)像素的面積渲染。

iOS 開發(fā)中,所有控件的坐標(biāo)以及控件大小都是以點(diǎn)為單位的,假如我在屏幕上需要展示一張 20 * 20 (單位:point)大小的圖片,那么設(shè)計(jì)師應(yīng)該怎么給我圖呢?這里就會(huì)用到屏幕模式的概念,如果屏幕是 2x,那么就需要提供 40 * 40 (單位: pixel)大小的圖片,如果屏幕是 3x,那么就提供 60 * 60 大小的圖片,且圖片的命名需要遵守以下規(guī)范:
Standard:<ImageName><device_modifier>.<filename_extension>
High resolution:<ImageName>@2x<device_modifier>.<filename_extension>
High HD resolution:<ImageName>@3x<device_modifier>.<filename_extension>

ImageName: 圖片名字,根據(jù)場(chǎng)景命名
device_modifier: 可選,可以是 ~ipad 或者 ~iphone, 當(dāng)需要為 iPad 和 iPhone 分別指定一套圖時(shí)需要加上此字段
filename_extension: 圖片后綴名,iOS中使用 png 圖片

例如:
MyImage.png - 1x 顯示屏自動(dòng)加載的圖片版本
MyImage@2x.png - 2x 顯示屏自動(dòng)加載的圖片版本
MyImage@3x.png - 3x 顯示屏自動(dòng)加載的圖片版本
MyImage@2x~iphone.png - 2x iPhone 和 iPod touch 顯示屏自動(dòng)加載的圖片版本
MyImage@3x~iphone.png - 3x iPhone and iPod 顯示屏自動(dòng)加載的圖片版本

  • 2x屏幕的設(shè)備會(huì)自動(dòng)加載 xxx@2x.png 命名的圖片資源,3x屏幕的設(shè)備會(huì)自動(dòng)加載 xxx@3x.png 的圖片, 現(xiàn)在基本沒有 1x屏幕的設(shè)備了,可以不用提供這個(gè)分辨率的圖片了。

1.2 設(shè)計(jì)和開發(fā)之間的多屏適配問題

  • 現(xiàn)在APP設(shè)計(jì)開發(fā)必須考慮適配大、中、小三種屏幕。所以如何做到交付一套設(shè)計(jì)稿解決適配大中小三屏的問題?設(shè)計(jì)和開發(fā)之間采用什么協(xié)作模式?

一個(gè)基本思路是:

  1. 選擇一種尺寸作為設(shè)計(jì)和開發(fā)基準(zhǔn);
  2. 定義一套適配規(guī)則,自動(dòng)適配剩下兩種尺寸;
  3. 特殊適配效果給出設(shè)計(jì)效果。

更多詳情可以參考這篇文章:手機(jī)淘寶的設(shè)計(jì)方案

  • 參考手機(jī)淘寶的設(shè)計(jì)方案如下:


    image
  1. 第一步,視覺設(shè)計(jì)階段,設(shè)計(jì)師按寬度750px(iPhone 6)做設(shè)計(jì)稿,除圖片外所有設(shè)計(jì)元素用矢量路徑來做。設(shè)計(jì)定稿后在750px的設(shè)計(jì)稿上做標(biāo)注,輸出標(biāo)注圖。同時(shí)等比放大1.5倍生成寬度1125px的設(shè)計(jì)稿,在1125px的稿子里切圖。
  2. 第二步,輸出兩個(gè)交付物給開發(fā)工程師:一個(gè)是程序用到的@3x切圖資源,另一個(gè)是寬度750px的設(shè)計(jì)標(biāo)注圖。
  3. 第三步,開發(fā)工程師拿到750px標(biāo)注圖和@3x切圖資源,完成iPhone 6(375pt)的界面開發(fā)。此階段不能用固定寬度的方式開發(fā)界面,得用自動(dòng)布局(auto layout),方便后續(xù)適配到其它尺寸。
  4. 第四步,適配調(diào)試階段,基于iPhone 6的界面效果,分別向上向下調(diào)試iPhone 6 plus(414pt)和iPhone 5S及以下(320pt)的界面效果。由此完成大中小三屏適配。
  • 為什么選擇iPhone 6作為基準(zhǔn)尺寸?

當(dāng)面對(duì)大中小三種屏幕需要適配的時(shí)候,很容易想到先做好一種屏幕,再去適配剩下兩種屏幕。第一個(gè)決定是到底以哪種屏幕作為設(shè)計(jì)和開發(fā)的基準(zhǔn)尺寸。我們選擇中間尺寸的iPhone 6(750px/375pt)作為基準(zhǔn),基于幾個(gè)原因:

  1. 從中間尺寸向上和向下適配的時(shí)候界面調(diào)整的幅度最小。375pt下的設(shè)計(jì)效果適配到414pt和320pt偏差不會(huì)太大。假設(shè)以414pt為基準(zhǔn)做出很優(yōu)雅的設(shè)計(jì),到320pt可能元素之間比例就不是那么回事了,比如圖片和文字之間視覺比例可能失調(diào)。
  2. iPhone 6 plus有兩種顯示模式,標(biāo)準(zhǔn)模式分辨率為1242x2208,放大模式分辨率為1125x2001(即iPhone 6的1.5倍)。可見官方系統(tǒng)里iPhone 6和iPhone 6 plus分辨率之間就存在1.5倍的倍率關(guān)系。很多情況下這兩種尺寸可以用1.5倍直接等比適配。
  3. 1242x2208這個(gè)奇葩的數(shù)值是蘋果官方都不愿意公開宣傳的一個(gè)分辨率,不便于記憶和計(jì)算柵格。640x1136雖然是廣泛應(yīng)用的一個(gè)分辨率,但是大屏?xí)r代依然以小尺寸為設(shè)計(jì)基準(zhǔn)顯然不合時(shí)宜,設(shè)計(jì)師會(huì)停留在小屏的視角做設(shè)計(jì).
    所以,iPhone6的750x1334是最適合基準(zhǔn)尺寸

1.3 開發(fā)時(shí)適配規(guī)范

  • 適配規(guī)則:文字流式,控件彈性,圖片等比縮放。
    適配規(guī)則

控件彈性指的是,navigation、cell、bar等適配過程中垂直方向上高度不變;水平方向?qū)挾茸兓瘯r(shí),通過調(diào)整元素間距或元素右對(duì)齊的方式實(shí)現(xiàn)自適應(yīng)。這樣屏幕越大,在垂直方向上可以顯示更多內(nèi)容,發(fā)揮大屏幕的優(yōu)勢(shì)。

2. IOS 屏幕適配代碼實(shí)現(xiàn)

2.1 布局處理

2.1.1 masonary布局適配實(shí)例

  • 在使用masonary自動(dòng)布局時(shí),可以根據(jù)6s的屏幕設(shè)計(jì),設(shè)置一個(gè)比例系數(shù),比如

//以6/6s為準(zhǔn)寬度縮小系數(shù)

#define kJLXWidthScale [UIScreen mainScreen].bounds.size.height/375.0 

//高度縮小系數(shù)

#define kJLXHeightScale [UIScreen mainScreen].bounds.size.height/667.0 
  • 這樣在布局的的時(shí)候,可以考慮使用上這個(gè)系數(shù)設(shè)置高度
UIButton *createrButton = [[UIButton alloc] init];

[self.view addSubview:createrButton];

UIEdgeInsets padding = UIEdgeInsetsMake(0, 10, 65, 10);

[createrButton setBackgroundImage:[UIImage imageNamed:@"common_button_pink"] forState:UIControlStateNormal];

[createrButton mas_makeConstraints:^(MASConstraintMaker *make){ 

 make.left.equalTo(self.view.mas_left).with.offset(padding.left);

 make.height.equalTo(@(60*kJLXHeightScale));

 make.bottom.equalTo(self.view.mas_bottom).with.offset(-padding.bottom);

 make.right.equalTo(self.view.mas_right).with.offset(-padding.right);

}];
  • 這樣在5s小屏手機(jī)上面,按鈕的高度就會(huì)根據(jù)比例系數(shù)來動(dòng)態(tài)調(diào)整大小。

2.1.2 Jimu 1.0 用到的布局適配函數(shù)

  • 之前Jimu 1.0中用到的布局轉(zhuǎn)換主要通過下面這個(gè)函數(shù)來轉(zhuǎn)換實(shí)際的寬度或高度:

橫屏下,水平方向適配函數(shù)

/// 設(shè)備橫屏下,水平方向適配·
///
/// - Parameters:
///   - iPhone6Scale: iPhone6 水平方向@2x尺寸
///   - iPadScale: 分辨率比例為768*1024的iPad 水平方向@2x尺寸
/// - Returns: 適配后的尺寸
func layoutHorizontal(iPhone6 iPhone6Scale: Float, iPad iPadScale: Float) -> Float {
    
    let iphoneWidth = iPhone6Scale / 2
    let iPadWidth = iPadScale / 2
    
    var newWidth: Float = 0
    
    switch Device.type() {
    case .iPhone4:
        newWidth = iphoneWidth * (480.0 / 667.0)
    case .iPhone5:
        newWidth = iphoneWidth * (568.0 / 667.0)
    case .iPhone6:
        newWidth = iphoneWidth
    case .iPhone6p:
        newWidth = iphoneWidth * (736.0 / 667.0)
    case .iPhoneX:
        newWidth = iphoneWidth * ((812.0 - 78) / 667.0)
    case .iPhoneXR:
        newWidth = iphoneWidth * ((896.0 - 78) / 667.0)
    case .iPad_768_1024:
        newWidth = iPadWidth
    case .iPad_834_1112:
        newWidth = iPadWidth * (1112.0 / 1024.0)
    case .iPad_1024_1366:
        newWidth = iPadWidth * (1366.0 / 1024.0)
    }
    
    return newWidth
}

設(shè)備橫屏下,垂直方向適配函數(shù)

/// 設(shè)備橫屏下,垂直方向適配
///
/// - Parameters:
///   - iPhone6Scale: iPhone6 垂直方向@2x尺寸
///   - iPadScale: 分辨率比例為768*1024的iPad 垂直方向@2x尺寸
/// - Returns: 適配后的尺寸
func layoutVertical(iPhone6 iPhone6Scale: Float, iPad iPadScale: Float) -> Float {
    
    let iphoneHeight = iPhone6Scale / 2
    let iPadHeight = iPadScale / 2
    
    var newHeight: Float = 0
    
    switch Device.type() {
    case .iPhone4:
        newHeight = iphoneHeight * (320.0 / 375.0)
    case .iPhone5:
        newHeight = iphoneHeight * (320.0 / 375.0)
    case .iPhone6:
        newHeight = iphoneHeight
    case .iPhone6p:
        newHeight = iphoneHeight * (414.0 / 375.0)
    case .iPhoneX:
        newHeight = iphoneHeight * (375.0 / 375.0)
    case .iPhoneXR:
        newHeight = iphoneHeight * (414.0 / 375.0)
    case .iPad_768_1024:
        newHeight = iPadHeight
    case .iPad_834_1112:
        newHeight = iPadHeight * (834.0 / 768.0)
    case .iPad_1024_1366:
        newHeight = iPadHeight * (1024.0 / 768.0)
    }
    
    return newHeight
}

  • 這種適配方式,可以滿足橫屏下適配各種設(shè)備,但是所有布局的代碼都需要調(diào)用這兩個(gè)函數(shù),浸入性很強(qiáng)。所以需要優(yōu)化一下。

2.1.3 布局適配優(yōu)化

2.1.3.1 增加判斷設(shè)備類型的extension擴(kuò)展

  • 先來看一下之前Jimu 1.0 是通過一個(gè)自定義枚舉來實(shí)現(xiàn)的,這樣的不好的地方也是浸入性很強(qiáng),每個(gè)調(diào)用的地方都需要用這個(gè)枚舉值。
/// 獲取設(shè)備型號(hào)
enum Device {
    
    case iPhone4            /// 4/4s          320*480  @2x
    case iPhone5            /// 5/5C/5S/SE    320*568  @2x
    case iPhone6            /// 6/6S/7/8      375*667  @2x
    case iPhone6p           /// 6P/6SP/7P/8P  414*736  @3x
    case iPhoneX            /// X             375*812   @3x
    //    case iPhoneXS           /// XS            375*812   @3x (同X)
    case iPhoneXR           /// XR            414*896   @2x (放大模式下為 375*812)
    //    case iPhoneXSMAX        /// XSMAX         414*896   @3x (同XR)
    
    
    case iPad_768_1024      /// iPad(5th generation)/iPad Air/iPad Air2/iPad pro(9.7)  768*1024  @2x
    case iPad_834_1112      /// iPad pro(10.5)  834*1112   @2x
    case iPad_1024_1366     /// iPad pro(12.9)  1024*1366  @2x
    
    
    /// 判斷具體設(shè)備
    ///
    /// - Returns: 具體設(shè)備名
    static func type() -> Device {
        
        switch screenWidth {
        case 480.0:
            return .iPhone4
        case 568.0:
            return .iPhone5
        case 667.0:
            return .iPhone6
        case 736.0:
            return .iPhone6p
        case 812.0:
            return .iPhoneX
        case 896.0:
            return .iPhoneXR
        case 1024.0:
            return .iPad_768_1024
        case 1112.0:
            return .iPad_834_1112
        case 1366.0:
            return .iPad_1024_1366
        default:
            return .iPad_768_1024
        }
    }
    
    /// 判斷是否為iPad
    ///
    /// - Returns: true 是, false 否
    static func isIPad() -> Bool  {
        //        print("() = \(self.type())")
        return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad)
    }
    
    static func isIPhone5() -> Bool {
        return Device.type() == Device.iPhone5 ? true : false
    }
    
    static var safeAreaInsets: UIEdgeInsets {
        if #available(iOS 11.0, *) {
            return UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
        }
        return .zero
    }
    
    static var safeScreenWidth: CGFloat {
        return UIScreen.main.bounds.width-safeAreaInsets.left-safeAreaInsets.right
    }
    
    static var safeScreenHeight: CGFloat {
        return UIScreen.main.bounds.height-safeAreaInsets.top-safeAreaInsets.bottom
    }
    
}
  • 將上面設(shè)備類型判斷代碼優(yōu)化為UIDevice的一個(gè)類擴(kuò)展
extension UIDevice {
    
    func Version()->String{
        
        let appVersion: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
        return appVersion
    }
    
    
    @objc public class func isiPhoneX() -> Bool {
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 1125, height: 2436)))! {
            return true
        }
        return false
    }
    
    public class func isiPhone6PlusBigMode() -> Bool {
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 1125, height: 2001)))! {
            return true
        }
        return false
    }
    
    public class func isiPhone6Plus() -> Bool {
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width:1242, height: 2208)))! {
            return true
        }
        return false
    }
    
    public class func isiPhone6BigMode() -> Bool{
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 320, height: 568)))! {
            return true
        }
        return false
    }
    
    public class func isiPhone6() -> Bool {
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width:750, height: 1334)))! {
            return true
        }
        return false
    }
    
    public class func isiPhone5() -> Bool {
        if (UIScreen.main.currentMode?.size.equalTo(CGSize.init(width: 640, height: 1136)))! {
            return true
        }
        return false
    }
    
    public class func isiOS11() -> Bool {
        if #available(iOS 11.0, *) {
            return true
        } else {
            return false
        }
    }
    
    public class func isiOS10() -> Bool {
        if #available(iOS 10.0, *) {
            return true
        } else {
            return false
        }
    }
    
    public class func isiOS9() -> Bool {
        if #available(iOS 9.0, *) {
            return true
        } else {
            return false
        }
    }
    
    public class func isiOS8() -> Bool {
        if #available(iOS 8.0, *) {
            return true
        } else {
            return false
        }
    }
    
    public class func isAiPad() -> Bool {
        if UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad {
            return true
        }
        return false
    }
}

  • 然后為了簡(jiǎn)化調(diào)用,可以定義一個(gè)全局變量
// MARK: - 判斷 機(jī)型
let isiPhone5 = UIDevice.isiPhone5()
let isiPhone6 = UIDevice.isiPhone6()
let isiPhone6BigModel = UIDevice.isiPhone6BigMode()
let isiPhone6Plus = UIDevice.isiPhone6Plus()
let isiPhone6PlusBigMode = UIDevice.isiPhone6PlusBigMode()
let isiPhoneX = UIDevice.isiPhoneX()
let isIpad = UIDevice.isAiPad()

// MARK: - 系統(tǒng)類型
let kisiOS11 = UIDevice.isiOS11()
let kisiOS10 = UIDevice.isiOS10()
let kisiOS9 = UIDevice.isiOS9()
let kisiOS8 = UIDevice.isiOS8()
  • 定義全局變量簡(jiǎn)化屏幕寬度,高度計(jì)算
let screenWidth = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width)
let screenHeight = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width)
let screenBounds = UIScreen.main.bounds

2.1.3.2 增加 NSInteger 類擴(kuò)展

extension NSInteger {
    /// iphone 5 上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i5(-30) 等價(jià)于 -(60.i5(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 5 上的大小
    /// - Returns: isiPhone5 ? size : CGFloat(self)
    func i5(_ size: CGFloat) -> CGFloat {
        return isiPhone5 ? size : CGFloat(self)
    }
    
    /// iphone 6 放大模式上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i6BigModel(-30) 等價(jià)于 -(60.i6BigModel(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6 放大模式 上的大小
    /// - Returns: isiPhone6BigModel ? size : CGFloat(self)
    func i6BigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6BigModel ? size : CGFloat(self)
    }
    
    /// iphone 6p 放大模式上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i6PBigModel(-30) 等價(jià)于 -(60.i6PBigModel(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6p 放大模式 上的大小
    /// - Returns: isiPhone6PlusBigMode ? size  : CGFloat(self)
    func i6PBigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6PlusBigMode ? size : CGFloat(self)
    }
    
    /// iphone x 上的大小
    /// ?? 《*注意運(yùn)算順序 -60.ix(-30) 等價(jià)于 -(60.ix(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone x 上的大小
    /// - Returns: isiPhoneX ? size / 2.0 : CGFloat(self)
    func ix(_ size: CGFloat) -> CGFloat {
        return isiPhoneX ? size : CGFloat(self)
    }
    
    /// ipad
    /// ?? 《*注意運(yùn)算順序 -60.ipad(-30) 等價(jià)于 -(60.ipad(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: ipad 上的大小
    /// - Returns: isIpad ? size : CGFloat(self)
    func ipad(_ size: CGFloat) -> CGFloat {
        return isIpad ? size : CGFloat(self)
    }
    
    /// 比例縮放 width
    ///
    /// - Parameter size: origin width
    /// - Returns: 比例縮放后的 width 沒有除以2.0
    func scaleW() -> CGFloat {
        return (screenWidth / 375 * CGFloat(self))
    }
    /// 比例縮放 height result沒有除以2.0
    ///
    /// - Parameter size: origin height
    /// - Returns: 比例縮放后的 height 沒有除以2.0
    func scaleH() -> CGFloat {
        return (screenHeight / 667 * CGFloat(self))
    }
}

2.1.3.3 增加 CGFloat 類擴(kuò)展

extension CGFloat {
    
    /// iphone 5 上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i5(-30) 等價(jià)于 -(60.i5(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 5 上的大小
    /// - Returns: isiPhone5 ? size : self
    func i5(_ size: CGFloat) -> CGFloat {
        return isiPhone5 ? size : self
    }
    
    /// iphone 6 放大模式上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i6BigModel(-30) 等價(jià)于 -(60.i6BigModel(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6 放大模式 上的大小
    /// - Returns: isiPhone6BigModel ?  : self
    func i6BigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6BigModel ? size : self
    }
    
    /// iphone 6p 放大模式上的大小
    /// ?? 《*注意運(yùn)算順序 -60.i6PBigModel(-30) 等價(jià)于 -(60.i6PBigModel(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6p 放大模式 上的大小
    /// - Returns: isiPhone6PlusBigMode ? size : self
    func i6PBigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6PlusBigMode ? size : self
    }
    
    /// iphone x上的大小
    /// ?? 《*注意運(yùn)算順序 -60.ix(-30) 等價(jià)于 -(60.ix(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone x 上的大小
    /// - Returns: isiPhoneX ? size  : self
    func ix(_ size: CGFloat) -> CGFloat {
        return isiPhoneX ? size : self
    }
    
    /// ipad 上的大小
    /// ?? 《*注意運(yùn)算順序 -60.ipad(-30) 等價(jià)于 -(60.ipad(-30)) 結(jié)果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: ipad 上的大小
    /// - Returns: isIpad ? size : self
    func ipad(_ size: CGFloat) -> CGFloat {
        return isIpad ? size : self
    }
    
    
    /// 比例縮放 width
    ///
    /// - Parameter size: origin width
    /// - Returns: 比例縮放后的 width 沒有除以2.0
    func scaleW() -> CGFloat {
        return (screenWidth / 375 * self)
    }
    /// 比例縮放 height
    ///
    /// - Parameter size: origin height
    /// - Returns: 比例縮放后的 height 沒有除以2.0
    func scaleH() -> CGFloat {
        return (screenHeight / 667 * self)
    }
}

2.1.3.4 增加 Bool 類擴(kuò)展

extension Bool {
    /// iphone 5 上的大小
    ///
    /// - Parameter size: iphone 5 上的大小
    /// - Returns: isiPhone5 ? size : self
    func i5(_ size: Bool) -> Bool {
        return isiPhone5 ? size : self
    }
    
    /// iphone 6 放大模式上的大小
    ///
    /// - Parameter size: iphone 6 放大模式 上的大小
    /// - Returns: isiPhone6BigModel ? size : self
    func i6BigModel(_ size: Bool) -> Bool {
        return isiPhone6BigModel ? size : self
    }
    
    /// iphone 6p 放大模式上的大小
    ///
    /// - Parameter size: iphone 6p 放大模式 上的大小
    /// - Returns: isiPhone6PlusBigMode ? size  : self
    func i6PBigModel(_ size: Bool) -> Bool {
        return isiPhone6PlusBigMode ? size : self
    }
    
    /// iphone x 上的大小
    ///
    /// - Parameter size: iphone x 上的大小
    /// - Returns: isiPhoneX ? size / 2.0 : self
    func ix(_ size: Bool) -> Bool {
        return isiPhoneX ? size : self
    }
    
    /// ipad
    ///
    /// - Parameter size: ipad 上的大小
    /// - Returns: isIpad ? size : self
    func ipad(_ size: Bool) -> Bool {
        return isIpad ? size : self
    }
}

2.2 圖片適配處理

  • 在項(xiàng)目中經(jīng)常有這樣的需求:如下圖,截取一部分拉伸,其他不變
image

實(shí)現(xiàn)代碼如下:

UIImage *img = [UIImage imageNamed:@"popup"];

img = [img resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 55) resizingMode:UIImageResizingModeStretch];

self.resizableImgView.image = img;

swift 代碼如下

/// 從中間拉伸圖片
    ///
    /// - Parameter image: 拉伸之前原始圖
    /// - Returns: 拉伸后圖片
    static func stretchFromCenter(image: UIImage?) -> UIImage? {
        guard let oriImage = image else {
            return nil
        }
        let result = oriImage.resizableImage(withCapInsets: UIEdgeInsetsMake(oriImage.size.height/2, oriImage.size.width/2, oriImage.size.height/2, oriImage.size.width/2), resizingMode: .stretch)
        return result
    }
  • 平鋪圖片:即一張小圖可以平鋪為多張小圖


    平鋪圖片

    實(shí)現(xiàn)代碼如下:

UIImage *img = [UIImage imageNamed:@"about"];

img = [img resizableImageWithCapInsets:UIEdgeInsetsMake(0, 11.5, 0, 11) resizingMode:UIImageResizingModeTile];

self.resizableImgView.image = img;
  • 通過純顏色創(chuàng)建圖片
/// 通過純色創(chuàng)建圖片
    ///
    /// - Parameter color: 顏色
    /// - Returns: 通過純顏色創(chuàng)建的圖片
    static func createImage(with color: UIColor) -> UIImage {
        let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
        UIGraphicsBeginImageContext(rect.size)
        let ctx = UIGraphicsGetCurrentContext()
        guard let context = ctx else { return UIImage() }
        context.setFillColor(color.cgColor)
        context.fill(rect)
        let theImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return theImage ?? UIImage()
    }

2.3 文字適配處理

2.3.1 根據(jù)字符串計(jì)算寬度,高度

  • 自動(dòng)適配原則上UILabel都是不設(shè)置高度的,根據(jù)文字內(nèi)容自動(dòng)適配高度。這個(gè)時(shí)候我們經(jīng)常需要用到根據(jù)文字String 字符串來計(jì)算整個(gè)字符串的寬度和高度。

jimu 1.0 用到的計(jì)算方法如下:

extension String {

func calculateSize(_ size: CGSize, font: UIFont) -> CGSize {
        let paragraphStyle = NSMutableParagraphStyle()
        //        paragraphStyle.lineSpacing = 7
        paragraphStyle.lineBreakMode = .byCharWrapping
        let attributes = [NSAttributedStringKey.font:font, NSAttributedStringKey.paragraphStyle:paragraphStyle.copy()]
        let expectedLabelSize = (self as NSString).boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes, context: nil).size
        return expectedLabelSize
    }
    

    func getWidth(font: UIFont) -> CGFloat {
        let attrs = [NSAttributedStringKey.font : font]
       return (self as NSString).boundingRect(with: CGSize.zero, options: .usesLineFragmentOrigin, attributes: attrs, context: nil).size.width
    }
}

// 計(jì)算文字高度或者寬度與weight參數(shù)無關(guān)
extension String {
    func ga_widthForComment(fontSize: CGFloat, height: CGFloat = 15) -> CGFloat {
        let font = UIFont.systemFont(ofSize: fontSize)
        let rect = NSString(string: self).boundingRect(with: CGSize(width: CGFloat(MAXFLOAT), height: height), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
        return ceil(rect.width)
    }
    
    func ga_heightForComment(fontSize: CGFloat, width: CGFloat) -> CGFloat {
        let font = UIFont.systemFont(ofSize: fontSize)
        let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
        return ceil(rect.height)
    }
    
    func ga_heightForComment(fontSize: CGFloat, width: CGFloat, maxHeight: CGFloat) -> CGFloat {
        let font = UIFont.systemFont(ofSize: fontSize)
        let rect = NSString(string: self).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
        return ceil(rect.height)>maxHeight ? maxHeight : ceil(rect.height)
    }
}

2.3.2 UIColor 轉(zhuǎn)換

extension UIColor {
    
    
    /// RGB顏色
    ///
    /// - Parameters:
    ///   - red: R
    ///   - green: G
    ///   - blue: B
    ///   - alpha: A
    convenience init(red:Int, green:Int, blue:Int, alpha:CGFloat = 1.0) {
        self.init(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: alpha)
    }
    
    
    /// 16進(jìn)制顏色
    ///
    /// - Parameters:
    ///   - rgb: RGB Int值
    ///   - alpha: 透明度
    convenience init(hex rgb:Int, alpha:CGFloat = 1.0) {
        self.init(red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF, alpha: alpha)
    }

    
    /// 隨機(jī)顏色
    ///
    /// - Parameter randomAlpha: 是否隨機(jī)透明度,默認(rèn)false
    /// - Returns: 隨機(jī)顏色
    public static func random(randomAlpha: Bool = false) -> UIColor {
        let randomRed = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
        let randomGreen = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
        let randomBlue = CGFloat(Float(arc4random()) / 0xFFFFFFFF)
        let alpha = randomAlpha ? CGFloat(Float(arc4random()) / 0xFFFFFFFF) : 1.0
        return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: alpha)
    }
    
    /// Hex String -> UIColor
    convenience init(hexString: String, alpha: CGFloat = 1.0) {
        let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
        let scanner = Scanner(string: hexString)
        
        if hexString.hasPrefix("#") {
            scanner.scanLocation = 1
        }
        
        var color: UInt32 = 0
        scanner.scanHexInt32(&color)
        
        let mask = 0x000000FF
        let r = Int(color >> 16) & mask
        let g = Int(color >> 8) & mask
        let b = Int(color) & mask
        
        let red   = CGFloat(r) / 255.0
        let green = CGFloat(g) / 255.0
        let blue  = CGFloat(b) / 255.0
        
        self.init(red: red, green: green, blue: blue, alpha: alpha)
    }
}

2.3.3

2.4 特殊控件適配處理

3. IOS 最新系統(tǒng)適配問題

  • 蘋果官方資料:
  1. WWDC19視頻
  2. Xcode 11 beta 下載
  3. macOS Catalina 10.15 beta 下載

3.1 IOS 13 適配

3.1.1 即將廢棄的 LaunchImage

從 iOS 8 的時(shí)候,蘋果就引入了 LaunchScreen,我們可以設(shè)置 LaunchScreen來作為啟動(dòng)頁。當(dāng)然,現(xiàn)在你還可以使用LaunchImage來設(shè)置啟動(dòng)圖。不過使用LaunchImage的話,要求我們必須提供各種屏幕尺寸的啟動(dòng)圖,來適配各種設(shè)備,隨著蘋果設(shè)備尺寸越來越多,這種方式顯然不夠 Flexible。而使用 LaunchScreen的話,情況會(huì)變的很簡(jiǎn)單, LaunchScreen是支持AutoLayout+SizeClass的,所以適配各種屏幕都不在話下。

  • <font face="黑體" color=red size=3>注意啦??,從2020年4月開始,所有使? iOS13 SDK的 App將必須提供 LaunchScreen,LaunchImage即將退出歷史舞臺(tái)*</font>。

3.1.2 Sign in with Apple -提供第三方登錄的注意啦

<font face="黑體" color=red size=3>如果你的應(yīng)用使用了第三方登錄,那么你可能也需要加下 「Sign in with Apple」
Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.</font>

3.1.3 iOS 13 DeviceToken有變化

NSString *dt = [deviceToken description];
dt = [dt stringByReplacingOccurrencesOfString: @"<" withString: @""];
dt = [dt stringByReplacingOccurrencesOfString: @">" withString: @""];
dt = [dt stringByReplacingOccurrencesOfString: @" " withString: @""];
<font face="黑體" color=red size=3>這段代碼運(yùn)行在 iOS 13 上已經(jīng)無法獲取到準(zhǔn)確的DeviceToken字符串了,iOS 13 通過[deviceToken description]獲取到的內(nèi)容已經(jīng)變了。</font>

  • 解決方案
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    if (![deviceToken isKindOfClass:[NSData class]]) return;
    const unsigned *tokenBytes = [deviceToken bytes];
    NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                          ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                          ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                          ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
    NSLog(@"deviceToken:%@",hexToken);
}

3.1.4 MPMoviePlayerController 在iOS 13已經(jīng)不能用了

<font face="黑體" color=red size=3>'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.' </font>

  • 解決方案:

既然不能再用了,那只能換掉了。替代方案就是AVKit里面的那套播放器。

3.1.5 控制器的 modalPresentationStyle 默認(rèn)值變了

查閱了下 UIModalPresentationStyle枚舉定義,赫然發(fā)現(xiàn)iOS 13新加了一個(gè)枚舉值:

typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
    UIModalPresentationFullScreen = 0,
    UIModalPresentationPageSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
    UIModalPresentationFormSheet API_AVAILABLE(ios(3.2)) API_UNAVAILABLE(tvos),
    UIModalPresentationCurrentContext API_AVAILABLE(ios(3.2)),
    UIModalPresentationCustom API_AVAILABLE(ios(7.0)),
    UIModalPresentationOverFullScreen API_AVAILABLE(ios(8.0)),
    UIModalPresentationOverCurrentContext API_AVAILABLE(ios(8.0)),
    UIModalPresentationPopover API_AVAILABLE(ios(8.0)) API_UNAVAILABLE(tvos),
    UIModalPresentationBlurOverFullScreen API_AVAILABLE(tvos(11.0)) API_UNAVAILABLE(ios) API_UNAVAILABLE(watchos),
    UIModalPresentationNone API_AVAILABLE(ios(7.0)) = -1,
    UIModalPresentationAutomatic API_AVAILABLE(ios(13.0)) = -2,
};
  • 解決方案
  1. 如果你完全接受蘋果的這個(gè)默認(rèn)效果,那就不需要去修改任何代碼。
  2. 如果,你原來就比較細(xì)心,已經(jīng)設(shè)置了modalPresentationStyle的值,那你也不會(huì)有這個(gè)影響。
  3. 對(duì)于想要找回原來默認(rèn)交互的同學(xué),直接設(shè)置如下即可:
    self.modalPresentationStyle = UIModalPresentationOverFullScreen;

3.1.6 UITextField 的私有屬性 _placeholderLabel 被禁止訪問了

  • IOS 13下調(diào)用下面代碼會(huì)導(dǎo)致閃退
[self.textField setValue:self.placeholderColor forKeyPath:@"_placeholderLabel.textColor"];

打印錯(cuò)誤信息如下:

<font face="黑體" color=red size=1>'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug' </font>

  • 解決方案:
UITextField有個(gè)attributedPlaceholder的屬性,我們可以自定義這個(gè)富文本來達(dá)到我們需要的結(jié)果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;

<font face="黑體" color=red size=2>iOS 13 通過 KVC 方式修改私有屬性,有 Crash 風(fēng)險(xiǎn),謹(jǐn)慎使用!并不是所有KVC都會(huì)Crash,要嘗試!</font>

3.1.7 UISearchBar顯示問題

  • SearchBar的高度只有1px
  1. <font face="黑體" color=red size=2>升級(jí)到iOS13,UISearchController上的SearchBar顯示異常,查看后發(fā)現(xiàn)對(duì)應(yīng)的高度只有1px,目前沒找到具體導(dǎo)致的原因,</font>
  2. 解決辦法是: 使用KVO監(jiān)聽frame值變化后設(shè)置去應(yīng)該顯示的高度
  • 黑線處理crash
  1. <font face="黑體" color=red size=2>之前為了處理搜索框的黑線問題會(huì)遍歷后刪除UISearchBarBackground,在iOS13會(huì)導(dǎo)致UI渲染失敗crash;</font>
  2. 解決辦法是: 設(shè)置UISearchBarBackground的layer.contents為nil
  • TabBar紅點(diǎn)偏移
  1. <font face="黑體" color=red size=2>如果之前有通過TabBar上圖片位置來設(shè)置紅點(diǎn)位置,在iOS13上會(huì)發(fā)現(xiàn)顯示位置都在最左邊去了。遍歷UITabBarButton的subViews發(fā)現(xiàn)只有在TabBar選中狀態(tài)下才能取到UITabBarSwappableImageView,</font>
  2. 解決辦法是: 修改為通過UITabBarButton的位置來設(shè)置紅點(diǎn)的frame

3.1.8 黑暗模式 Dark Mode

Apps on iOS 13 are expected to support dark mode
Use system colors and materials
Create your own dynamic colors and images Leverage flexible infrastructure

UI 需要出一套新交互

  • 在iOS13,為UIViewController和UIView擴(kuò)展了一個(gè)新的API-overrideUserInterfaceStyle,使用方法,官方文檔大致是這么說的:
  1. 通過設(shè)置overrideUserInterfaceStyle屬性以使該視圖及其子視圖具有特定的UIUserInterfaceStyle。但如果想要獲取當(dāng)前的UIUserInterfaceStyle,需要改用traitCollection.userInterfaceStyle。
  2. 盡可能使用UIViewController上的overrideUserInterfaceStyle屬性。僅在以下時(shí)間使用此屬性:
    (1) 在單個(gè)視圖或小視圖層次結(jié)構(gòu)上局部使用特定樣式。
    (2) 您希望在整個(gè)UIWindow及其視圖控制器和模態(tài)彈出的ViewController上使用特定樣式,且不希望強(qiáng)制更改整個(gè)應(yīng)用程序具有樣式。 (如果您確實(shí)希望整個(gè)應(yīng)用程序具有某種樣式,請(qǐng)不要使用它,而是在Info.plist中設(shè)置UIUserInterfaceStyle鍵。)
  3. 當(dāng)設(shè)置在普通的UIView上時(shí):
    此屬性僅影響此視圖及其子視圖的特征。
    它不會(huì)影響任何視圖控制器或其他視圖控制器的子視圖。
  4. 在UIWindow上設(shè)置時(shí):
    此屬性會(huì)影響rootViewController,從而影響整個(gè)視圖控制器和視圖層次結(jié)構(gòu)。
    它還會(huì)影響該window模態(tài)出來的界面。
  • 由此可見,overrideUserInterfaceStyle不僅會(huì)影響自己,還會(huì)影響自己的子視圖,換做window就會(huì)影響整個(gè)window中的所有視圖及視圖控制器,包括模態(tài)跳轉(zhuǎn)出來的視圖控制器。
    而且,文檔中也特別強(qiáng)調(diào)了,你可以設(shè)置整個(gè)應(yīng)用程序只是用某種樣式,具體方法可以通過代碼,也可以通過info.plist配置鍵User Interface Style,對(duì)應(yīng)的ValueLight/Dark。
if #available(iOS 13.0, *) {
    window?.overrideUserInterfaceStyle = .light;
}
image

3.1.8.1 適配黑暗模式

  • 適配Dark 模式主要從這幾個(gè)方面:
  1. 模擬器調(diào)試(simulator debug)
  2. 圖片(assets)
  3. 顏色(color)
  4. 狀態(tài)欄(status bar)
3.1.8.1.1 模擬器調(diào)試
  • 運(yùn)行項(xiàng)目,點(diǎn)擊Xcode底部調(diào)試欄中Environment Overrides.
  • 開啟Interface Style,就可以切換了。如下圖:


    切換Dark模式

    模擬器打開darkMode
3.1.8.1.2 圖片適配
  • 圖片適配,主要是我們本地圖片資源適配,網(wǎng)絡(luò)圖片的話,還是比較繁瑣。
  • 圖片適配比較方便的就是通過Assets.xcassets進(jìn)行圖片管理:
  1. 添加一個(gè)image set,重命名如"adaptimage",選中該image set;
  2. 選中Attributes Inspector;
  3. 將Appearances由"None"改為"Any,Dark";
  4. 不同模式下設(shè)置不同圖片即可,mode 改變會(huì)自動(dòng)選擇不同的圖片


    Assets.xcassets進(jìn)行圖片管理
  • 當(dāng)然圖片適配,你也可以直接使用判斷當(dāng)前系統(tǒng)mode的方式進(jìn)行區(qū)分,就我個(gè)人而言不是很喜歡這種方式,因?yàn)檫€需要監(jiān)聽系統(tǒng)模式的變化,重寫UITraitEnvironment協(xié)議方法traitCollectionDidChange(_:),我們先看下協(xié)議方法:
/** Trait environments expose a trait collection that describes their environment. */
public protocol UITraitEnvironment : NSObjectProtocol {

    @available(iOS 8.0, *)
    var traitCollection: UITraitCollection { get }

    /** To be overridden as needed to provide custom behavior when the environment's traits change. */
    @available(iOS 8.0, *)
    func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
}

  • 最后,我們只需要在改變系統(tǒng)mode的時(shí)候,重寫代理:
func updateImageView() {
    let image = traitCollection.userInterfaceStyle == .light ? UIImage(named: "dark-ios") : UIImage(named: "white-ios")
    imageView.image = image
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    updateImageView()
}
3.1.8.1.3 顏色適配
  • 顏色適配有三種方式:
  • 方法一:是通過Assets.xcassets添加一個(gè)Color Set,目前系統(tǒng)支持≥iOS11.0
extension UIColor {
    @available(iOS 11.0, *)
    public /*not inherited*/ init?(named name: String) // load from main bundle

    @available(iOS 11.0, *)
    public /*not inherited*/ init?(named name: String, in bundle: Bundle?, compatibleWith traitCollection: UITraitCollection?)
}
colorset-darkmode
  • 方法二:代碼創(chuàng)建動(dòng)態(tài)顏色init(dynamicProvider: @escaping (UITraitCollection) -> UIColor),目前系統(tǒng)支持≥iOS 13.0
// 方法二
let titleColor = UIColor.init(dynamicProvider: { (trait) -> UIColor in
    return trait.userInterfaceStyle == .light ? UIColor.black : UIColor.white
})
btn.setTitleColor(titleColor, for: .normal)
  • 方法三:像圖片一樣,監(jiān)聽模式轉(zhuǎn)變,重寫traitCollectionDidChange(_:)方法,不推薦這種。
3.1.8.1.4 狀態(tài)欄適配
  • 目前狀態(tài)欄也增加了一種模式,由之前的兩種,變成了三種, 其中default由之前的黑色內(nèi)容,變成了會(huì)根據(jù)系統(tǒng)模式,自動(dòng)選擇當(dāng)前展示lightContent還是darkContent。
public enum UIStatusBarStyle : Int {
    case `default` // Automatically chooses light or dark content based on the user interface style

    @available(iOS 7.0, *)
    case lightContent // Light content, for use on dark backgrounds

    @available(iOS 13.0, *)
    case darkContent // Dark content, for use on light backgrounds
}
  • 我們?cè)谑褂玫臅r(shí)候,就可以重寫preferredStatusBarStyle的get方法:
override var preferredStatusBarStyle: UIStatusBarStyle{
    get{
        return .lightContent
    }
}

3.1.9 模態(tài)彈出默認(rèn)交互改變

iOS 13 的 presentViewController 默認(rèn)有視差效果,模態(tài)出來的界面現(xiàn)在默認(rèn)都下滑返回。 一些頁面必須要點(diǎn)確認(rèn)才能消失的,需要適配。如果項(xiàng)目中頁面高度全部是屏幕尺寸,那么多出來的導(dǎo)航高度會(huì)出現(xiàn)問題。

/*
 Defines the presentation style that will be used for this view controller when it is presented modally. Set this property on the view controller to be presented, not the presenter.
 If this property has been set to UIModalPresentationAutomatic, reading it will always return a concrete presentation style. By default UIViewController resolves UIModalPresentationAutomatic to UIModalPresentationPageSheet, but other system-provided view controllers may resolve UIModalPresentationAutomatic to other concrete presentation styles.
 Defaults to UIModalPresentationAutomatic on iOS starting in iOS 13.0, and UIModalPresentationFullScreen on previous versions. Defaults to UIModalPresentationFullScreen on all other platforms.
 */
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle API_AVAILABLE(ios(3.2));
  • 解決方案:
// Swift
self.modalPresentationStyle = .fullScreen
 
// Objective-C
self.modalPresentationStyle = UIModalPresentationFullScreen;

3.1.10 App啟動(dòng)過程中,部分View可能無法實(shí)時(shí)獲取到frame

可能是為了優(yōu)化啟動(dòng)速度,App 啟動(dòng)過程中,部分View可能無法實(shí)時(shí)獲取到正確的frame

  • 解決方案
// 只有等執(zhí)行完 UIViewController 的 viewDidAppear 方法以后,才能獲取到正確的值,在viewDidLoad等地方 frame Size 為 0,例如:
 [[UIApplication sharedApplication] statusBarFrame];

更多關(guān)于IOS的變化參考:iOS13AdaptationTips

<font face="黑體" color=green size=5>少壯不努力,老大徒悲傷</font>

參考博客:http://www.itdecent.cn/p/75f34462bd9a

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容