天天看點

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

IOS 螢幕适配(一)理論篇

  • 1. IOS 螢幕适配基本概念
    • 1.1 IOS 裝置的尺寸和分辨率
      • 1.1.1 分辨率相關概念
      • 1.1.2 IOS 各個裝置對應的分辨率
    • 1.2 設計和開發之間的多屏适配問題
    • 1.3 開發時适配規範
    • 1.4 開發UI 的注意事項
      • 1.4.1 xib VS 純代碼
  • 2. IOS 螢幕适配代碼實作
    • 布局簡介
      • Auto Layout基礎概念
      • 限制 Constraint
      • InterfaceBuilder
      • StoryBoard版限制
      • VFL的使用
      • 使用第三方架構
      • 動态布局
    • 2.1 自動布局代碼實作
      • 2.1.1 masonary布局适配執行個體
      • 2.1.2 Jimu 1.0 用到的布局适配函數
      • 2.1.3 布局适配優化
        • 2.1.3.1 增加判斷裝置類型的extension擴充
        • 2.1.3.2 增加 NSInteger 類擴充
        • 2.1.3.3 增加 CGFloat 類擴充
        • 2.1.3.4 增加 Bool 類擴充
    • 2.2 圖檔适配處理
    • 2.3 文字适配處理
      • 2.3.1 字型集
      • 2.3.2 根據字元串計算寬度,高度
      • 2.3.3 UIColor 轉換
      • 2.3.4 使用 SwiftyFitsize适配視圖和字型大小
        • 2.3.4.1 SwiftyFitsize安裝
        • 2.3.4.2 SwiftyFitsize 使用簡介
        • 2.3.4.3 SwiftyFitsize 使用詳解
          • 2.3.4.3.1 操作符 `~` 與 `≈`
          • 2.3.4.3.2 xib 和 storyboard 使用
          • 2.3.4.3.3 使用執行個體
    • 2.4 第三方布局架構SnapKit使用注意事項
      • 2.4.1 布局之前一定要先添加到父視圖上否則會引起崩潰
      • 2.4.2 修改限制
        • 2.4.2.1 引用限制(References)
        • 2.4.2.2 使用updateConstraints進行限制更新
        • 2.4.2.3 使用remarkConstraints修改限制
        • 2.4.2.4 updateConstraints使用不當,導緻閃退問題
        • 2.4.2.5 更新限制問題
          • 2.4.2.5.1 依賴限制的更新問題
      • 2.4.3 snapkit更改布局動畫效果
      • 2.4.4 兩個自适應寬度控件,導緻右邊的自适應問題
  • 3. IOS 最新系統适配問題
    • 3.1 IOS 13 适配
      • 3.1.1 即将廢棄的 LaunchImage
      • 3.1.2 Sign in with Apple -提供第三方登入的注意啦
      • 3.1.3 iOS 13 DeviceToken有變化
      • 3.1.4 MPMoviePlayerController 在iOS 13已經不能用了
      • 3.1.5 控制器的 modalPresentationStyle 預設值變了
      • 3.1.6 UITextField 的私有屬性 _placeholderLabel 被禁止通路了
      • 3.1.7 UISearchBar顯示問題
      • 3.1.8 黑暗模式 Dark Mode
        • 3.1.8.1 适配黑暗模式
          • 3.1.8.1.1 模拟器調試
          • 3.1.8.1.2 圖檔适配
          • 3.1.8.1.3 顔色适配
          • 3.1.8.1.4 狀态欄适配
      • 3.1.9 模态彈出預設互動改變
      • 3.1.10 App啟動過程中,部分View可能無法實時擷取到frame

1. IOS 螢幕适配基本概念

  • 本篇部落格對應demo下載下傳:IOS螢幕适配demo

1.1 IOS 裝置的尺寸和分辨率

1.1.1 分辨率相關概念

  • 點(Points):
是iOS開發中引入的抽象機關,稱作點。開發過程中所有基于坐标系的繪制都是以 point 作為機關,在iPhone 2G,3G,3GS的年代,point 和螢幕上的像素是完全一一對應的,即 320 * 480 (points), 也是 320 * 480 (pixels)
  • 渲染像素 (Rendered Pixels):
Rendered Pixels: 渲染像素, 以 point 為機關的繪制最終都會渲染成 pixels,這個過程被稱為光栅化。基于 point 的坐标系乘以比例因子可以得到基于像素的坐标系,高比例因子會使更多的細節展示,目前的比例因子會是 1x,2x,3x
  • 實體像素(Physical Pixels):
Physical Pixels: 實體像素,就是裝置螢幕實際的像素
  • 裝置螢幕的實體長度(Physical Device):
Physical Device: 裝置螢幕的實體長度,使用英寸作為機關。比如iPhone 4螢幕是3.5英寸,iPhone 5 是4英寸,iphone 6是4.7英寸,這裡的數字是指手機螢幕對角線的實體長度。實際上會是Physical Pixels的像素值(而不是Rendered Pixels的像素值)會渲染到該螢幕上, 螢幕會有 PPI(pixels-per-inch) 的特性,PPI 的值告訴你每英寸會有多少像素渲染。

1.1.2 IOS 各個裝置對應的分辨率

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • 更多關于IOS裝置分辨率可以看The Ultimate Guide To iPhone Resolutions
機型 螢幕寬高(point) 渲染像素(pixel) 實體像素(pixel) 螢幕對角線長度(英寸) 螢幕模式
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裝置尺寸
機型 螢幕寬高(point) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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 尺寸
機型 螢幕寬高(point) 螢幕模式(Scale) 實體像素(pixel) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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尺寸
機型 螢幕寬高(point) 螢幕模式(Scale) 實體像素(pixel) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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 尺寸
機型 螢幕寬高(point) 螢幕模式(Scale) 實體像素(pixel) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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尺寸
機型 螢幕寬高(point) 螢幕模式(Scale) 實體像素(pixel) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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尺寸
機型 螢幕寬高(point) 螢幕模式(Scale) 實體像素(pixel) 比例 像素密度(PPI) 螢幕尺寸 型号代碼 釋出日
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 的含義:
螢幕模式,描述的就是螢幕中一個點有多少個 Rendered Pixels 渲染,對于2倍屏(又稱 Retina 顯示屏),會有 2 * 2 = 4 個像素的面積渲染,對于3倍屏(又稱 Retina HD 顯示屏),會有 3 * 3 = 9 個像素的面積渲染。

iOS 開發中,所有控件的坐标以及控件大小都是以點為機關的,假如我在螢幕上需要展示一張 20 * 20 (機關:point)大小的圖檔,那麼設計師應該怎麼給我圖呢?這裡就會用到螢幕模式的概念,如果螢幕是 2x,那麼就需要提供 40 * 40 (機關: pixel)大小的圖檔,如果螢幕是 3x,那麼就提供 60 * 60 大小的圖檔,且圖檔的命名需要遵守以下規範:

Standard:<device_modifier>.<filename_extension>

High resolution:@2x<device_modifier>.<filename_extension>

High HD resolution:@3x<device_modifier>.<filename_extension>

ImageName: 圖檔名字,根據場景命名

device_modifier: 可選,可以是 ~ipad 或者 ~iphone, 當需要為 iPad 和 iPhone 分别指定一套圖時需要加上此字段

filename_extension: 圖檔字尾名,iOS中使用 png 圖檔

例如:

MyImage.png - 1x 顯示屏自動加載的圖檔版本

[email protected] - 2x 顯示屏自動加載的圖檔版本

[email protected] - 3x 顯示屏自動加載的圖檔版本

[email protected]~iphone.png - 2x iPhone 和 iPod touch 顯示屏自動加載的圖檔版本

[email protected]~iphone.png - 3x iPhone and iPod 顯示屏自動加載的圖檔版本

  • 2x螢幕的裝置會自動加載 [email protected] 命名的圖檔資源,3x螢幕的裝置會自動加載 [email protected] 的圖檔, 現在基本沒有 1x螢幕的裝置了,可以不用提供這個分辨率的圖檔了。

1.2 設計和開發之間的多屏适配問題

  • 現在APP設計開發必須考慮适配大、中、小三種螢幕。是以如何做到傳遞一套設計稿解決适配大中小三屏的問題?設計和開發之間采用什麼協作模式?
一個基本思路是:
  1. 選擇一種尺寸作為設計和開發基準;
  2. 定義一套适配規則,自動适配剩下兩種尺寸;
  3. 特殊适配效果給出設計效果。

更多詳情可以參考這篇文章:手機淘寶的設計方案

  • 參考手機淘寶的設計方案如下:
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  1. 第一步,視覺設計階段,設計師按寬度750px(iPhone 6)做設計稿,除圖檔外所有設計元素用矢量路徑來做。設計定稿後在750px的設計稿上做标注,輸出标注圖。同時等比放大1.5倍生成寬度1125px的設計稿,在1125px的稿子裡切圖。
  2. 第二步,輸出兩個傳遞物給開發工程師:一個是程式用到的@3x切圖資源,另一個是寬度750px的設計标注圖。
  3. 第三步,開發工程師拿到750px标注圖和@3x切圖資源,完成iPhone 6(375pt)的界面開發。此階段不能用固定寬度的方式開發界面,得用自動布局(auto layout),友善後續适配到其它尺寸。
  4. 第四步,适配調試階段,基于iPhone 6的界面效果,分别向上向下調試iPhone 6 plus(414pt)和iPhone 5S及以下(320pt)的界面效果。由此完成大中小三屏适配。
  • 為什麼選擇iPhone 6作為基準尺寸?
當面對大中小三種螢幕需要适配的時候,很容易想到先做好一種螢幕,再去适配剩下兩種螢幕。第一個決定是到底以哪種螢幕作為設計和開發的基準尺寸。我們選擇中間尺寸的iPhone 6(750px/375pt)作為基準,基于幾個原因:
  1. 從中間尺寸向上和向下适配的時候界面調整的幅度最小。375pt下的設計效果适配到414pt和320pt偏差不會太大。假設以414pt為基準做出很優雅的設計,到320pt可能元素之間比例就不是那麼回事了,比如圖檔和文字之間視覺比例可能失調。
  2. iPhone 6 plus有兩種顯示模式,标準模式分辨率為1242x2208,放大模式分辨率為1125x2001(即iPhone 6的1.5倍)。可見官方系統裡iPhone 6和iPhone 6 plus分辨率之間就存在1.5倍的倍率關系。很多情況下這兩種尺寸可以用1.5倍直接等比适配。
  3. 1242x2208這個奇葩的數值是蘋果官方都不願意公開宣傳的一個分辨率,不便于記憶和計算栅格。640x1136雖然是廣泛應用的一個分辨率,但是大屏時代依然以小尺寸為設計基準顯然不合時宜,設計師會停留在小屏的視角做設計.

    是以,iPhone6的750x1334是最适合基準尺寸

1.3 開發時适配規範

  • 适配規則:文字流式,控件彈性,圖檔等比縮放。
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
控件彈性指的是,navigation、cell、bar等适配過程中垂直方向上高度不變;水準方向寬度變化時,通過調整元素間距或元素右對齊的方式實作自适應。這樣螢幕越大,在垂直方向上可以顯示更多内容,發揮大螢幕的優勢。

1.4 開發UI 的注意事項

1.4.1 xib VS 純代碼

  • 蘋果一直用xib來标榜他們家app開發的簡單易上手:将各種你需要的東西往螢幕上一拖一放,一個UI界面就搞定了,這很cool不是嘛!
  • xib的優點顯而易見:
  1. 易上手、可視化,所見即所得
  2. 減少代碼量
  3. 快,适合小app快速開發
  • 但是在我們的實際項目中,是不推薦使用xib的,下面來分析原因:
  1. 首先,xib本身過于笨拙,隻能搭建一些簡單的UI,動态性很差,難以滿足app複雜的UI互動需求。
  2. 其次,做過性能優化的同學都知道,xib(or StoryBoard)的性能是很差的,相對于用純代碼alloc的元件來說,xib加載慢,而且會占用app包的體積。不僅僅是app的性能,使用老mac打開較大的xib檔案,有時候會卡的你懷疑人生,嚴重影響開發效率(心情)。
  3. 除此以外,對于團隊協作來說,xib也不是一個好選項:閱讀困難,無法在git上檢視曆史改動,容易造成沖突,造成沖突後難以解決,元素通過outlets與代碼的連結難以維護,容易在改動中造成錯漏等等。
  • 當然我們不是要完全禁用了xib,用代碼寫UI的缺點也很明顯:繁瑣,代碼量大,是以對一些元素較多,又比較固定的UI元件,我們可以用xib來減少代碼量。
  • 針對UI代碼繁瑣,重複編碼多的情況,我們可以通過适當封裝(UI工廠類),組織結構(MVC,分離UI代碼)等手段,清晰邏輯。

    例如下面代碼用工廠方法擷取一個UILabel

// label 工廠方法
+ (UILabel *)labelWithFont:(UIFont *)font
                     color:(UIColor *)
                      text:(NSString *)text
             attributeText:(NSAttributeString *)attributeText
                 alignment:(NSTextAlignment)alignment;
           

2. IOS 螢幕适配代碼實作

布局簡介

  • 一般常見的布局方式有:
  1. 固定間距:在不同尺寸下,間距總是固定
  2. 流式布局:文字,圖檔等在不同螢幕下流式排布,比如大屏下一行顯示四張圖檔,小屏一行三張,圖檔尺寸固定
  3. 比例放大:間距,文字大小,圖檔大小等比例放大
  4. 保持比值:兩個UI元素或者圖檔的長寬等屬性保持一定的比值
  5. 對齊:元素間按某個方向對齊

Auto Layout基礎概念

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

從iOS7開始蘋果在Cocoa平台引入AutoLayout進行UI的基本布局,但是AutoLayout非常反人類,不僅代碼繁瑣而且使用不靈活限制很多。

比如我想要把三個元素等間距地展示在螢幕上,用AutoLayout寫完基本蛋都碎了,更别說動态地在兩套布局間切換這種進階需求。

後來蘋果推出sizeClass,試圖解決多套布局的問題,但是仍然沒有觸及到碼農的痛點,而且依賴xib使它泛用性不好。

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • AutoLayout比較适合:
  1. 基本的對齊(上下左右對齊,居中對齊等)
  2. 固定的布局,固定的間距,動态性不高的頁面
  3. 簡單且數量較少的UI元素
  • 不擅長:
  1. 比例布局
  2. 動态性較強的頁面局部
  3. 不同螢幕大小比例的适配
  4. 複雜的UI
AutoLayout對性能是有損耗的,是以對性能有要求的場景,比如清單中的cell,推薦用代碼計算frame,提高滑動幀率。

限制 Constraint

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

InterfaceBuilder

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

StoryBoard版限制

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

VFL的使用

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
//
//  CodeViewController.m

#import "CodeViewController.h"

@interface CodeViewController ()
@property (weak, nonatomic) IBOutlet UIButton *btnGreen;
@property (weak, nonatomic) IBOutlet UIButton *btnRed;

@end

@implementation CodeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /*
     @param toItem: 跟哪個view建立關系
    */
    //for btn green
    NSLayoutConstraint *constraintCenterXGreen = [NSLayoutConstraint constraintWithItem:_btnGreen attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
    //防止沖突
    _btnGreen.translatesAutoresizingMaskIntoConstraints = false;
    //父子關系,添加到父視圖(較高層級的視圖)上
    [self.view addConstraint:constraintCenterXGreen];
    
    //VFL語言
    //V代表縱向 |代表父視圖
    NSArray *constraintsForGreen = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[_btnGreen]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_btnGreen)];
    [self.view addConstraints:constraintsForGreen];
    
    //for btn red
    NSLayoutConstraint *constraintCenterXRed = [NSLayoutConstraint constraintWithItem:_btnRed attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
    //防止沖突
    _btnRed.translatesAutoresizingMaskIntoConstraints = false;
    //父子關系,添加到父視圖(較高層級的視圖)上
    [self.view addConstraint:constraintCenterXRed];
    
    //VFL語言
    NSArray *constraintsForRed = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_btnGreen]-20-[_btnRed]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_btnRed,_btnGreen)];
    [self.view addConstraints:constraintsForRed];
}

@end
           

使用第三方架構

  • OC下一般使用 Masonry
  • Swift 一般使用 SnapKit

動态布局

UI界面是動态的,在不同狀态,不同尺寸或者手機的橫豎屏情況下,我們往往需要在多套布局方案中切換,或者對布局進行微調。如果使用xib布局的話,可以使用SizeClass+AutoLayout的方案;如果是代碼實作的頁面,則沒有官方提供的工具,隻能用邏輯去判斷。
  • 一般來說,我們寫複雜的UI頁面,需要遵循兩個原則:
  1. UI布局代碼,要清晰:這是最重要的,要一眼就知道在調整那一塊,怎麼調整,如果不能,适當拆分,優化命名。
  2. 布局代碼要和業務邏輯獨立:在一些常用設計模式下,我們會将UI和資料模型解耦,在UI内部,同樣要将互動,配置這些邏輯和布局解耦,獨立出類似前端css這樣的純布局檔案。
  • 例如:将布局代碼提煉出來,在不同尺寸下調用不同的實作
if (IS_IPHONE_6){  
        self.layout = [MyLayout iPhone6Layout];
    }else if (IS_IPHONE_6_PLUS){  
        self.layout = [MyLayout iPhone6PlusLayout]; 
    }

    // 實作小螢幕布局
    + (MyLayout *)iPhone6Layout {...}
    // 實作大螢幕布局
    + (MyLayout *)iPhone6PlusLayout {...}
           

2.1 自動布局代碼實作

2.1.1 masonary布局适配執行個體

  • 在使用masonary自動布局時,可以根據6s的螢幕設計,設定一個比例系數,比如

//以6/6s為準寬度縮小系數

//高度縮小系數

  • 這樣在布局的的時候,可以考慮使用上這個系數設定高度
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小屏手機上面,按鈕的高度就會根據比例系數來動态調整大小。

2.1.2 Jimu 1.0 用到的布局适配函數

  • 之前Jimu 1.0中用到的布局轉換主要通過下面這個函數來轉換實際的寬度或高度:

橫屏下,水準方向适配函數

/// 裝置橫屏下,水準方向适配·
///
/// - 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
}
           

裝置橫屏下,垂直方向适配函數

/// 裝置橫屏下,垂直方向适配
///
/// - 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
}

           
  • 這種适配方式,可以滿足橫屏下适配各種裝置,但是所有布局的代碼都需要調用這兩個函數,浸入性很強。是以需要優化一下。

2.1.3 布局适配優化

2.1.3.1 增加判斷裝置類型的extension擴充

  • 先來看一下之前Jimu 1.0 是通過一個自定義枚舉來實作的,這樣的不好的地方也是浸入性很強,每個調用的地方都需要用這個枚舉值。
/// 擷取裝置型号
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
    
    
    /// 判斷具體裝置
    ///
    /// - Returns: 具體裝置名
    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
    }
    
}
           
  • 将上面裝置類型判斷代碼優化為UIDevice的一個類擴充
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
    }
}

           
  • 然後為了簡化調用,可以定義一個全局變量
// MARK: - 判斷 機型
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: - 系統類型
let kisiOS11 = UIDevice.isiOS11()
let kisiOS10 = UIDevice.isiOS10()
let kisiOS9 = UIDevice.isiOS9()
let kisiOS8 = UIDevice.isiOS8()
           
  • 定義全局變量簡化螢幕寬度,高度計算
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 類擴充

extension NSInteger {
    /// iphone 5 上的大小
    /// ? 《*注意運算順序 -60.i5(-30) 等價于 -(60.i5(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 5 上的大小
    /// - Returns: isiPhone5 ? size : CGFloat(self)
    func i5(_ size: CGFloat) -> CGFloat {
        return isiPhone5 ? size : CGFloat(self)
    }
    
    /// iphone 6 放大模式上的大小
    /// ? 《*注意運算順序 -60.i6BigModel(-30) 等價于 -(60.i6BigModel(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6 放大模式 上的大小
    /// - Returns: isiPhone6BigModel ? size : CGFloat(self)
    func i6BigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6BigModel ? size : CGFloat(self)
    }
    
    /// iphone 6p 放大模式上的大小
    /// ? 《*注意運算順序 -60.i6PBigModel(-30) 等價于 -(60.i6PBigModel(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6p 放大模式 上的大小
    /// - Returns: isiPhone6PlusBigMode ? size  : CGFloat(self)
    func i6PBigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6PlusBigMode ? size : CGFloat(self)
    }
    
    /// iphone x 上的大小
    /// ? 《*注意運算順序 -60.ix(-30) 等價于 -(60.ix(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone x 上的大小
    /// - Returns: isiPhoneX ? size / 2.0 : CGFloat(self)
    func ix(_ size: CGFloat) -> CGFloat {
        return isiPhoneX ? size : CGFloat(self)
    }
    
    /// ipad
    /// ? 《*注意運算順序 -60.ipad(-30) 等價于 -(60.ipad(-30)) 結果為 -(-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 類擴充

extension CGFloat {
    
    /// iphone 5 上的大小
    /// ? 《*注意運算順序 -60.i5(-30) 等價于 -(60.i5(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 5 上的大小
    /// - Returns: isiPhone5 ? size : self
    func i5(_ size: CGFloat) -> CGFloat {
        return isiPhone5 ? size : self
    }
    
    /// iphone 6 放大模式上的大小
    /// ? 《*注意運算順序 -60.i6BigModel(-30) 等價于 -(60.i6BigModel(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6 放大模式 上的大小
    /// - Returns: isiPhone6BigModel ?  : self
    func i6BigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6BigModel ? size : self
    }
    
    /// iphone 6p 放大模式上的大小
    /// ? 《*注意運算順序 -60.i6PBigModel(-30) 等價于 -(60.i6PBigModel(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone 6p 放大模式 上的大小
    /// - Returns: isiPhone6PlusBigMode ? size : self
    func i6PBigModel(_ size: CGFloat) -> CGFloat {
        return isiPhone6PlusBigMode ? size : self
    }
    
    /// iphone x上的大小
    /// ? 《*注意運算順序 -60.ix(-30) 等價于 -(60.ix(-30)) 結果為 -(-30) 或者 -60》
    ///
    /// - Parameter size: iphone x 上的大小
    /// - Returns: isiPhoneX ? size  : self
    func ix(_ size: CGFloat) -> CGFloat {
        return isiPhoneX ? size : self
    }
    
    /// ipad 上的大小
    /// ? 《*注意運算順序 -60.ipad(-30) 等價于 -(60.ipad(-30)) 結果為 -(-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 類擴充

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 圖檔适配處理

  • 在項目中經常有這樣的需求:如下圖,截取一部分拉伸,其他不變
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

實作代碼如下:

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
    }
           
  • 平鋪圖檔:即一張小圖可以平鋪為多張小圖
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
    實作代碼如下:
UIImage *img = [UIImage imageNamed:@"about"];

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

self.resizableImgView.image = img;
           
  • 通過純顔色建立圖檔
/// 通過純色建立圖檔
    ///
    /// - Parameter color: 顔色
    /// - Returns: 通過純顔色建立的圖檔
    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 文字适配處理

  • 在開發中我們經常會遇到需要動态設定字型的情況:
  1. 不同螢幕尺寸,或者橫豎屏,需要展示不同的字型大小。
  2. 為使用者提供了文章調節字型選項。
  3. App的不同語言版本,需要顯示的字型不一樣。

2.3.1 字型集

  • (1) 較為簡單的做法是用宏或者枚舉定義字型參數,針對不同尺寸的螢幕,我們拿到不同的值:
#ifdef IPHONE6
#define kChatFontSize 16.f
#else IPHONE6Plus
#define kChatFontSize 18.f
#endif
           
  • (2) 在對一些舊代碼做字型适配擴充的時候,直接修改源碼改動太多,容易混亂,可以采用runTime方法hack Label等控件的展示,替換原有的setFont方法:
+ (void)load{  
    
    Method newMethod = class_getClassMethod([self class], @selector(mySystemFontOfSize:));  
    Method method = class_getClassMethod([self class], @selector(systemFontOfSize:));  
    method_exchangeImplementations(newMethod, method);  
}  
  
+ (UIFont *)mySystemFontOfSize:(CGFloat)fontSize{  
    UIFont *newFont=nil;  
    if (IS_IPHONE_6){  
        newFont = [UIFont adjustFont:fontSize * IPHONE6_INCREMENT];  
    }else if (IS_IPHONE_6_PLUS){  
        newFont = [UIFont adjustFont:fontSize * IPHONE6PLUS_INCREMENT];  
    }else{  
        newFont = [UIFont adjustFont:fontSize];  
    }  
    return newFont;  
}  
           
  • 以上(1),(2)套路缺點顯而易見:不夠靈活,将邏輯分散,不便于維護,擴充性也不好。
  • 有一張比較好的優化方案:字型集。
  • 什麼是字型集呢?
我們在用Keynote或者Office的時候,軟體會提供一些段落樣式,定義了段落、标題、說明等文字的字型,我們可以在不同的段落樣式中切換,來直接改變整個文章的字型風格。
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • 聽上去和我們的需求是不是很像呢,我們在代碼中也是做類似的事情,将不同場景下的字型定義到一個Font Collection中:
@protocol XRFontCollectionProtocol <NSObject>

- (UIFont *)bodyFont; // 文章
- (UIFont *)chatFont; // 聊天
- (UIFont *)titleFont; // 标題
- (UIFont *)noteFont; // 說明
......
@end
           
  • 不同的場景,靈活選擇不同的字型集:
+ (id<XRFontCollectionProtocol>)currentFontCollection {
    
#ifdef IS_IPhone6
    return [self collectionForIPhone6];
#elif IS_IPhone6p
    return [self collectionForIPhone6Plus];
#endif
    return nil;
}

// set font
titleLabel.font = [[XRFontManager currentFontCollection] titleFont];
           
  • 适配新的螢幕或者場景,我們隻需要簡單地增加一套字型集就好了,可以很友善的管理app中的字型樣式,做動态切換也很簡單。

2.3.2 根據字元串計算寬度,高度

  • 自動适配原則上UILabel都是不設定高度的,根據文字内容自動适配高度。這個時候我們經常需要用到根據文字String 字元串來計算整個字元串的寬度和高度。

jimu 1.0 用到的計算方法如下:

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
    }
}

           
// 計算文字高度或者寬度與weight參數無關
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.3 UIColor 轉換

extension UIColor {
    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)
    }
    convenience init(hex rgb:Int, alpha:CGFloat = 1.0) {
        self.init(red: (rgb >> 16) & 0xFF, green: (rgb >> 8) & 0xFF, blue: rgb & 0xFF, alpha: alpha)
    }
    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.4 使用 SwiftyFitsize适配視圖和字型大小

  • 下載下傳位址:SwiftyFitsize

2.3.4.1 SwiftyFitsize安裝

  • SwiftyFitsize 很簡單,就一個檔案,可以直接拷貝到項目或者通過pod 安裝

2.3.4.2 SwiftyFitsize 使用簡介

  • SwiftyFitsize在預設狀況下所使用的參照寬度為iphone6的375 如果設計圖所選用裝置的寬度與預設值不同,可以在AppDelegate下初始化所參照的寬度
  • 具體裝置的分辨率可以檢視上面開始的介紹,這裡還是列舉部分,友善參考:
裝置 邏輯分辨率(point) 裝置分辨率(pixel)
SE 320x568 640x1136
6(S)/7/8 375x667 750x1334
6(S)+/7+/8+ 414x736 1080x1920
X(S) 375x812 1125x2436
XR 414x896 828x1792
XS Max 414x896 1242x2688
  • 使用是非常友善的,隻需要在Number、UIFont、CGPoint、CGSize、UIEdgeInsetsMake這些類型的值後面加上~即可
100~
UIFont.systemFont(ofSize: 14)~
CGPoint(x: 10, y: 10)~
CGSize(width: 100, height: 100)~
CGRect(x: 10, y: 10, width: 100, height: 100)~
UIEdgeInsetsMake(10, 10, 10, 10)~

           
  • xib / storyboard 字型适配
支援控件 UILabel UIButton UITextView UITextField
  • xib / storyboard 限制适配
注:~請不要互相嵌套使用,如

2.3.4.3 SwiftyFitsize 使用詳解

2.3.4.3.1 操作符

~

無論是 ~ 還是 ≈ 對 iPhone 的适配效果是一樣的。而對 iPad 而言,iPad 的寬度太大,使用 ≈ 還是會按寬度比例進行運算,就會顯示特别臃腫,這時使用 ~ 在顯示上就會比較合适。
  1. ~ 在 ≈ 的基礎上針對 iPad 的适配大小後再去乘上 iPadFitMultiple 。
  2. 一般情況下直接使用 ~ 即可。
  • ~

    : 當裝置為 iPad 時,适配後的值會與

    iPadFitMultiple

    相乘
100~
UIFont.systemFont(ofSize: 14)~
CGPoint(x: 10, y: 10)~
CGRect(x: 10, y: 10, width: 100, height: 100)~
UIEdgeInsetsMake(10, 10, 10, 10)~
           
  • : (option + x) 适配後的值不會與

    iPadFitMultiple

    相乘
100≈
UIFont.systemFont(ofSize: 14)≈
CGPoint(x: 10, y: 10)≈
CGRect(x: 10, y: 10, width: 100, height: 100)≈
UIEdgeInsetsMake(10, 10, 10, 10)≈
           
  • 修改

    參照寬度

    iPadFitMultiple

    可以調用以下方法
/// 設定參照的相關參數
///
/// - Parameters:
///   - width: 參照的寬度
///   - iPadFitMultiple: iPad 在适配後所得值的倍數 (0 , 1]
SwiftyFitsize.reference(width: 414, iPadFitMultiple: 0.5)
           
2.3.4.3.2 xib 和 storyboard 使用
  • SwiftyFitType類型
enum SwiftyFitType: Int {
    /// Original Value
    case none = 0
    /// ~
    case flexible = 1
    /// ≈
    case force = 2
}
           
  • 字型大小适配 Font Fitsize
支援的UI控件有:

UILabel

UIButton

UITextView

UITextField

IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • Constraint Fitsize
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • iPad 關于 ~ 與 ≈ 在使用上的對比
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
2.3.4.3.3 使用執行個體
import UIKit
import SwiftyFitsize
import LXFProtocolTool

class ViewController: UIViewController {
    
    fileprivate let fitLabel : UILabel = {
        let v = UILabel()
        v.font = UIFont.boldSystemFont(ofSize: 25)~
        v.text = "https://github.com/LinXunFeng"
        return v
    }()
    
    fileprivate let redView : UIView = {
        let v = UIView()
        v.backgroundColor = .red
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.addSubview(fitLabel)
        self.view.addSubview(redView)
        
        fitLabel.sizeToFit()
        let fitLabelX: CGFloat = 15
        let fitLabelY: CGFloat = 100
        var frame = fitLabel.frame
        frame.origin.x = fitLabelX~
        frame.origin.y = fitLabelY~
        fitLabel.frame = frame
        
        let redViewFrame = CGRect(
            x: fitLabelX,
            y: fitLabelY + 50,
            width: 100,
            height: 100)
        redView.frame = redViewFrame~
        
        let fitsizeView = FitsizeView.loadFromNib()
        self.view.addSubview(fitsizeView)
        fitsizeView.frame = CGRect(x: 0, y: redViewFrame~.maxY, width: 328~, height: 298~)
    }
}
           

2.4 第三方布局架構SnapKit使用注意事項

  • SnapKit,一個經典的Swift版的第三方庫,專門用于項目的自動布局。它的作者仍然是寫Objective-C的第三方庫Masonry的大牛 - @Robert Payne

2.4.1 布局之前一定要先添加到父視圖上否則會引起崩潰

  • make.leading

    make.trailing

    是前後的的意思,這個會牽涉到國際化的問題,有些國家是傳回鍵實在右邊,剛好和我們這兒相反,在這裡就需要用到這兩個屬性布局和:

    make.left

    make.right

    (效果一樣操作習慣都是左邊傳回時)
  • 簡單布局執行個體:
func initUI() -> Void {
        contentView.addSubview(mineIcon)
        contentView.addSubview(mineName)
        contentView.addSubview(timeName)
        contentView.addSubview(statusLabel)
  
        mineIcon.snp.makeConstraints { (make) in
            make.left.equalToSuperview().offset(15)
            make.height.width.equalTo(40)
            make.top.equalToSuperview().offset(10)
        }
        mineName.snp.makeConstraints { (make) in
            make.left.equalTo(mineIcon.snp.right)
                .offset(6)
            make.top.equalTo(mineIcon.snp.top)
                .offset(6)
            make.height.equalTo(18)
            make.width.equalTo(120)
        }
        timeName.snp.makeConstraints { (make) in
            make.left.equalTo(mineIcon.snp.right)
                .offset(6)
            make.top.equalTo(mineName.snp.bottom)
                .offset(2)
            make.height.equalTo(18)
            make.width.equalTo(160)
        }
        
        statusLabel.snp.makeConstraints { (make) in
            make.height.equalTo(20)
            make.width.equalTo(80)
            make.right.equalToSuperview().offset(-10)
            make.centerY.equalTo(mineName)
        }
    }


           

2.4.2 修改限制

2.4.2.1 引用限制(References)

  • 我們能夠聲明一個局部變量或者類屬性來引用我們想要修改的限制,也可以使用數組存儲引用多個限制,然後對引用的限制進行修改
var topConstraint: Constraint? = nil
 
...
 
// when making constraints
view1.snp.makeConstraints { (make) -> Void in
  self.topConstraint = make.top.equalTo(superview).offset(padding.top).constraint
  make.left.equalTo(superview).offset(padding.left)
}
 
...
// then later you can call
self.topConstraint.uninstall()
 
// or if you want to update the constraint
self.topConstraint.updateOffset(5)


           

2.4.2.2 使用updateConstraints進行限制更新

  • 如果我們僅僅隻想更新限制的常量值,那麼我們可以使用updateConstraints方法
// this is Apple's recommended place for adding/updating constraints
// this method can get called multiple times in response to setNeedsUpdateConstraints
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
override func updateConstraints() {
    self.growingButton.snp.updateConstraints { (make) -> Void in
        make.center.equalTo(self);
        make.width.equalTo(self.buttonSize.width).priority(250)
        make.height.equalTo(self.buttonSize.height).priority(250)
        make.width.lessThanOrEqualTo(self)
        make.height.lessThanOrEqualTo(self)
    }
    
   // according to Apple super should be called at end of method
	 super.updateConstraints()
}

           
注意:更新限制是基于現有的限制條件進行修改其常量值,如果不是對已有的限制進行修改,那麼會出錯,并且不需要将所有的限制重新寫一遍,隻需要修改想要更新的限制即可。

例如:一個UILabel控件,相對于父視圖左右邊距分别是10,并且居中顯示,添加限制如下:

label.snp.makeConstraints { (make) in
     make.centerY.equalToSuperview()
     make.left.right.equalToSuperview().inset(10)
 }
           

現在修改距離左右兩邊的限制為30,直接使用updateConstraints方法即可

label.snp.updateConstraints { (make) in
    make.left.right.equalToSuperview().inset(30)
}
           

2.4.2.3 使用remarkConstraints修改限制

  • 使用remarkConstraints跟使用makeConstriants相似,但是首先會将之前所有的限制都去除,然後重新添加相關的限制
func changeButtonPosition() {
  self.button.snp.remakeConstraints { (make) -> Void in 
    make.size.equalTo(self.buttonSize)
    if topLeft {
      make.top.left.equalTo(10)
    } else {
      make.bottom.equalTo(self.view).offset(-10)
      make.right.equalTo(self.view).offset(-10)
    }
  }
}
           
  • 開發的過程中,我們可能會碰到這樣的需求:一個UI控件事先已經添加好限制了,這時候我想修改改控件其中的一個限制或者多個限制,并且限制的對象也發生了改變。實作思路有2種:
  1. 使用remarkConstraints方法在對應的條件下添加限制
  2. 事先添加所需要的所有限制并對限制進行引用,然後配合Constraints的activate()方法和deactivate()方法,使用deactivate()處理暫時不需要的限制,在需要的時候activate()需要的限制。

    執行個體2.4.2.3.1:

    添加兩個UILabel和一個UISwitch控件,點選UISwitch修改對應的限制。初始化基本限制和頁面效果如下:

func setupConstraints() {
        label1.snp.makeConstraints { (make) in
            make.left.top.equalToSuperview().inset(50)
            make.size.equalTo(CGSize(width: 100, height: 100))
        }
        label2.snp.makeConstraints { (make) in
            make.left.size.equalTo(label1)
            make.top.equalTo(label1.snp.bottom).offset(50)
        }
        st.snp.makeConstraints { (make) in
            make.center.equalToSuperview()
        }
        st.addTarget(self, action: #selector(tapSwitch(_:)), for: .touchUpInside)
    }
           
  • 方法一:使用remarkConstraints方法修改限制,如果是點選打開,那麼将label2設定限制為距離父視圖的右邊為50,大小變為200,頂部相對于label1的底部,如果是關閉,那麼回到最初的限制
@objc func tapSwitch(_ st: UISwitch) {
        if st.isOn {
            label2.snp.remakeConstraints { (make) in
                make.right.equalToSuperview().offset(-50)
                make.size.equalTo(CGSize(width: 200, height: 200))
                make.top.equalTo(label1.snp.bottom)
            }
        } else {
            label2.snp.remakeConstraints { (make) in
                make.left.size.equalTo(label1)
                make.top.equalTo(label1.snp.bottom).offset(50)
            }
        }
    }
           

可以看到操作也挺簡單,直接使用remarkConstraints方法添加想要的限制即可。

  • 方法二:使用activate()和deactivate()

    1、需要使用變量引用對應的限制

var leftConstraint: Constraint!
var rightConstraint: Constraint!
var originalSizeConstraint: Constraint!
var orignalTopConstraint: Constraint!
var targetTopConstraint: Constraint!
var targetSizeConstraint: Constraint!
var originalConstraints: [Constraint] = []
var targetConstraints: [Constraint] = []
           

2、為label2添加所有的限制

這裡需要将目标限制降低優先級,不然會出現限制沖突,因為系統不知道什麼使用限制,當限制優先級被降低之後,會預設先使用高優先級。

label2.snp.makeConstraints { (make) in
    leftConstraint = make.left.equalTo(label1).constraint
    originalSizeConstraint = make.size.equalTo(label1).constraint
    orignalTopConstraint = make.top.equalTo(label1.snp.bottom).offset(50).constraint
 
    targetTopConstraint = make.top.equalTo(label1.snp.bottom).priority(999).constraint
    targetSizeConstraint =  make.size.equalTo(CGSize(width: 200, height: 200)).priority(999).constraint
    rightConstraint = make.right.equalToSuperview().offset(-50).priority(999).constraint
 }
           

3、使用數組存儲對應的限制,友善後續的操作,注意:失效目标限制,即去除多餘的限制

// 添加到對應的數組
 originalConstraints.append(leftConstraint)
 originalConstraints.append(orignalTopConstraint)
 originalConstraints.append(originalSizeConstraint)
 
 targetConstraints.append(targetTopConstraint)
 targetConstraints.append(targetSizeConstraint)
 targetConstraints.append(rightConstraint)
 
 // 失效之後的限制
 targetConstraints.forEach {$0.deactivate()}
           

4、點選開關修改限制

@objc func tapSwitch(_ st: UISwitch) {
     if st.isOn { // 失效原有限制,生效目标限制
         originalConstraints.forEach {$0.deactivate()}
         targetConstraints.forEach {$0.activate()}
     } else { // 失效目标限制,生效原有的限制
         targetConstraints.forEach {$0.deactivate()}
         originalConstraints.forEach {$0.activate()}
     }
}
           

2.4.2.4 updateConstraints使用不當,導緻閃退問題

  • 場景如下:view2參照view1 布局

    當我們想要将視圖view2參照更改到其他View的時候,

view2.snp.updateConstraints { (make) in
            make.top.equalTo(otherView.snp.bottom)
        }//會引起程式崩潰
           
  • 原因分析:
在使用snapkit 布局時不能修改參照
  • 解決方法:
使用remakeConstraints
view2.snp.remakeConstraints { (make) in
            make.top.equalTo(otherView.snp.bottom)
            make.height.equalTo(24)
            make.left.equalToSuperview().offset(15)
            make.right.equalToSuperview().offset(-10)
        }
           

2.4.2.5 更新限制問題

2.4.2.5.1 依賴限制的更新問題
  • 問題描述:

如果一個控件B有可能會不顯示,但是另外一個控件C在B 顯示的時 候會依賴B 來設定限制,而 B 依賴 A 設定限制

此時就會有依賴限制的更新問題

  • 解決辦法:
  1. 初始限制設定
//首先添加A,B,C 之間的限制--B,C 是 A 的子控件, B 在 C 上面, A 的高度根據 C 的底部确定
     var CBottomConstrains: Constrain?
    A.addSubview(B)
    A.addSubview(C)
    B.snp_makeConstraints { (make) -> Void in
        make.top.equalTo(A).offset(8)
        make.leading.trailing.equalTo(A)
    }
    C.snp_makeConstraints { (make) -> Void in
        //記錄C 的頂部限制
        CBottomConstrains =  make.top.equalTo(B.snp_bottom).constrain
        make.leading.trailing.equalTo(A)
    }
    A.snp_makeConstraints { (make) -> Void in
        make.bottom.equalTo(C)
    }
           
  1. 子控件大小變化時的限制更新
if B.hidden = true{
        //1.先解除安裝限制
        CBottomConstrains?.uninstall()
        //2.更新限制
        A.snp_makeConstraints { (make) -> Void in
            //3.當 B 不顯示時, C 的 top就跟 A 的top 對齊
            CBottomConstrains = make.top.equalTo(A.snp_top).constraint
        }
    }B.hidden = false{
        //1.先解除安裝限制
        CBottomConstrains?.uninstall()
        //2.更新限制
        C.snp_makeConstraints { (make) -> Void in
            //3.當 B 顯示時, C 的 top 就更 B 的 bottom 對齊
            CBottomConstrains = make.top.equalTo(B.snp_bottom).constraint
        }
    }
           

2.4.3 snapkit更改布局動畫效果

  • 一定要在更新晚布局後加:**layoutIfNeeded()**才會起作用
UIView.animate(withDuration: 0.25) {
            self.listTableView.snp.updateConstraints { (make) in
//                make.left.right.equalToSuperview()
//                make.top.equalToSuperview().offset(0)
                make.height.equalTo(heightMine)
            }
            self.layoutIfNeeded()
        }

//或者用
		UIView.animate(withDuration: 0.25) {
            self.listTableView.snp.remakeConstraints { (make) in
               make.left.right.equalToSuperview()
               make.top.equalToSuperview().offset(0)     			   	
               make.height.equalTo(heightMine)
            }
            self.layoutIfNeeded()
        }
           

2.4.4 兩個自适應寬度控件,導緻右邊的自适應問題

  • 解決方法:給左邊控件設定:

    setContentHuggingPriority

//setContentHuggingPriority可以通俗了解成“别扯我”的優先級,優先級越高,越不能被扯長。
        leftLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        leftLabel.snp.makeConstraints { (make) in
            make.left.equalToSuperview().offset(15)
            make.height.equalTo(20)
            make.top.equalToSuperview().offset(76)
        }
        righLabel.snp.makeConstraints { (make) in
            make.left.equalTo(leftLabel.snp.right).offset(4)
            make.height.equalTo(28)
            make.right.equalToSuperview().offset(-8)
            make.centerY.equalTo(leftLabel)
        } 
           

3. IOS 最新系統适配問題

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

3.1 IOS 13 适配

3.1.1 即将廢棄的 LaunchImage

從 iOS 8 的時候,蘋果就引入了 LaunchScreen,我們可以設定 LaunchScreen來作為啟動頁。當然,現在你還可以使用LaunchImage來設定啟動圖。不過使用LaunchImage的話,要求我們必須提供各種螢幕尺寸的啟動圖,來适配各種裝置,随着蘋果裝置尺寸越來越多,這種方式顯然不夠 Flexible。而使用 LaunchScreen的話,情況會變的很簡單, LaunchScreen是支援AutoLayout+SizeClass的,是以适配各種螢幕都不在話下。
  • 注意啦⚠️,從2020年4月開始,所有使⽤ iOS13 SDK的 App将必須提供 LaunchScreen,LaunchImage即将退出曆史舞台*。

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

如果你的應用使用了第三方登入,那麼你可能也需要加下 「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.

  • 如何內建 可以參考這篇部落格:Sign in with Apple

3.1.3 iOS 13 DeviceToken有變化

NSString *dt = [deviceToken description];

dt = [dt stringByReplacingOccurrencesOfString: @"<" withString: @""];

dt = [dt stringByReplacingOccurrencesOfString: @">" withString: @""];

dt = [dt stringByReplacingOccurrencesOfString: @" " withString: @""];

這段代碼運作在 iOS 13 上已經無法擷取到準确的DeviceToken字元串了,iOS 13 通過[deviceToken description]擷取到的内容已經變了。

  • 解決方案
- (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已經不能用了

‘MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.’
  • 解決方案:
既然不能再用了,那隻能換掉了。替代方案就是AVKit裡面的那套播放器。

3.1.5 控制器的 modalPresentationStyle 預設值變了

查閱了下 UIModalPresentationStyle枚舉定義,赫然發現iOS 13新加了一個枚舉值:
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. 如果你完全接受蘋果的這個預設效果,那就不需要去修改任何代碼。
  2. 如果,你原來就比較細心,已經設定了modalPresentationStyle的值,那你也不會有這個影響。
  3. 對于想要找回原來預設互動的同學,直接設定如下即可:

    self.modalPresentationStyle = UIModalPresentationOverFullScreen;

3.1.6 UITextField 的私有屬性 _placeholderLabel 被禁止通路了

  • IOS 13下調用下面代碼會導緻閃退

列印錯誤資訊如下:

‘Access to UITextField’s _placeholderLabel ivar is prohibited. This is an application bug’
  • 解決方案:
UITextField有個attributedPlaceholder的屬性,我們可以自定義這個富文本來達到我們需要的結果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;
           
iOS 13 通過 KVC 方式修改私有屬性,有 Crash 風險,謹慎使用!并不是所有KVC都會Crash,要嘗試!

3.1.7 UISearchBar顯示問題

  • SearchBar的高度隻有1px
  1. 更新到iOS13,UISearchController上的SearchBar顯示異常,檢視後發現對應的高度隻有1px,目前沒找到具體導緻的原因,
  2. 解決辦法是: 使用KVO監聽frame值變化後設定去應該顯示的高度
  • 黑線處理crash
  1. 之前為了處理搜尋框的黑線問題會周遊後删除UISearchBarBackground,在iOS13會導緻UI渲染失敗crash;
  2. 解決辦法是: 設定UISearchBarBackground的layer.contents為nil
  • TabBar紅點偏移
  1. 如果之前有通過TabBar上圖檔位置來設定紅點位置,在iOS13上會發現顯示位置都在最左邊去了。周遊UITabBarButton的subViews發現隻有在TabBar選中狀态下才能取到UITabBarSwappableImageView,
  2. 解決辦法是: 修改為通過UITabBarButton的位置來設定紅點的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擴充了一個新的API-overrideUserInterfaceStyle,使用方法,官方文檔大緻是這麼說的:
  1. 通過設定overrideUserInterfaceStyle屬性以使該視圖及其子視圖具有特定的UIUserInterfaceStyle。但如果想要擷取目前的UIUserInterfaceStyle,需要改用traitCollection.userInterfaceStyle。
  2. 盡可能使用UIViewController上的overrideUserInterfaceStyle屬性。僅在以下時間使用此屬性:

    (1) 在單個視圖或小視圖層次結構上局部使用特定樣式。

    (2) 您希望在整個UIWindow及其視圖控制器和模态彈出的ViewController上使用特定樣式,且不希望強制更改整個應用程式具有樣式。 (如果您确實希望整個應用程式具有某種樣式,請不要使用它,而是在Info.plist中設定UIUserInterfaceStyle鍵。)

  3. 當設定在普通的UIView上時:

    此屬性僅影響此視圖及其子視圖的特征。

    它不會影響任何視圖控制器或其他視圖控制器的子視圖。

  4. 在UIWindow上設定時:

    此屬性會影響rootViewController,進而影響整個視圖控制器和視圖層次結構。

    它還會影響該window模态出來的界面。

  • 由此可見,

    overrideUserInterfaceStyle

    不僅會影響自己,還會影響自己的子視圖,換做

    window

    就會影響整個

    window

    中的所有視圖及視圖控制器,包括模态跳轉出來的視圖控制器。

    而且,文檔中也特别強調了,你可以設定整個應用程式隻是用某種樣式,具體方法可以通過代碼,也可以通過

    info.plist

    配置鍵

    User Interface Style

    ,對應的

    Value

    Light/Dark

if #available(iOS 13.0, *) {
    window?.overrideUserInterfaceStyle = .light;
}
           
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題

3.1.8.1 适配黑暗模式

  • 适配Dark 模式主要從這幾個方面:
  1. 模拟器調試(simulator debug)
  2. 圖檔(assets)
  3. 顔色(color)
  4. 狀态欄(status bar)
3.1.8.1.1 模拟器調試
  • 運作項目,點選Xcode底部調試欄中Environment Overrides.
  • 開啟Interface Style,就可以切換了。如下圖:
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
3.1.8.1.2 圖檔适配
  • 圖檔适配,主要是我們本地圖檔資源适配,網絡圖檔的話,還是比較繁瑣。
  • 圖檔适配比較友善的就是通過Assets.xcassets進行圖檔管理:
  1. 添加一個image set,重命名如"adaptimage",選中該image set;
  2. 選中Attributes Inspector;
  3. 将Appearances由"None"改為"Any,Dark";
  4. 不同模式下設定不同圖檔即可,mode 改變會自動選擇不同的圖檔
    IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • 當然圖檔适配,你也可以直接使用判斷目前系統

    mode

    的方式進行區分,就我個人而言不是很喜歡這種方式,因為還需要監聽系統模式的變化,重寫

    UITraitEnvironment

    協定方法

    traitCollectionDidChange(_:)

    ,我們先看下協定方法:
/** 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?)
}

           
  • 最後,我們隻需要在改變系統mode的時候,重寫代理:
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

    添加一個

    Color Set

    ,目前系統支援

    ≥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?)
}
           
IOS 螢幕适配(一)理論篇1. IOS 螢幕适配基本概念2. IOS 螢幕适配代碼實作3. IOS 最新系統适配問題
  • 方法二:代碼建立動态顔色

    init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)

    ,目前系統支援

    ≥iOS 13.0

// 方法二
let titleColor = UIColor.init(dynamicProvider: { (trait) -> UIColor in
    return trait.userInterfaceStyle == .light ? UIColor.black : UIColor.white
})
btn.setTitleColor(titleColor, for: .normal)
           
  • 方法三:像圖檔一樣,監聽模式轉變,重寫

    traitCollectionDidChange(_:)

    方法,不推薦這種。
3.1.8.1.4 狀态欄适配
  • 目前狀态欄也增加了一種模式,由之前的兩種,變成了三種, 其中

    default

    由之前的黑色内容,變成了會根據系統模式,自動選擇目前展示

    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
}
           
  • 我們在使用的時候,就可以重寫preferredStatusBarStyle的get方法:
override var preferredStatusBarStyle: UIStatusBarStyle{
    get{
        return .lightContent
    }
}
           

3.1.9 模态彈出預設互動改變

iOS 13 的

presentViewController

預設有視差效果,模态出來的界面現在預設都下滑傳回。 一些頁面必須要點确認才能消失的,需要适配。如果項目中頁面高度全部是螢幕尺寸,那麼多出來的導航高度會出現問題。
/*
 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啟動過程中,部分View可能無法實時擷取到frame

可能是為了優化啟動速度,App 啟動過程中,部分View可能無法實時擷取到正确的frame
  • 解決方案
// 隻有等執行完 UIViewController 的 viewDidAppear 方法以後,才能擷取到正确的值,在viewDidLoad等地方 frame Size 為 0,例如:
 [[UIApplication sharedApplication] statusBarFrame];
           

更多關于IOS的變化參考:iOS13AdaptationTips

少壯不努力,老大徒悲傷

參考部落格:https://www.jianshu.com/p/75f34462bd9a

https://www.jianshu.com/p/5e171975225a