iOS11 適配

灰s發表於2017-12-22

一、NavigationBar

1. UIBarItem

UIBarItem在iOS11在中新增landscapeImagePhone屬性,用來在小圖看不清楚的情況下,長按顯示放大的圖。在storyboard中也支援這個設定,對於HUD的image需要設定另一個iOS11新增的屬性:largeContentSizeImage,關於這部分更詳細的討論,可以參考 WWDC2017 Session 215:What's New in Accessibility

2. 返回按鈕的適配

iOS11之前移除返回按鈕文字的程式碼:

[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];
複製程式碼

iOS 11 中該方法依舊可以把文字移走,但是會往下偏一點,很不美觀,並且編輯器會報出如下類似的警告

iOS11.png

我的解決方法是將push方法包一層,自定義每個vc的navigationItem.backBarButtonItem,這種方式的時候,在childViewControllers使用push方法的時候會失效,需要使用當前viewController進行push操作

func dzy_push(_ vc:UIViewController, hide:Bool = false, animated:Bool = true) {
        if hide {
            vc.hidesBottomBarWhenPushed = hide
        }
        let btn = UIButton(type: .custom)
        btn.setImage(UIImage(named: "navi_back"), for: .normal)
        btn.imageEdgeInsets = UIEdgeInsetsMake(0, -20, 0, 0)
        btn.frame = CGRect(x: 0, y: 0, width: 25, height: 25)
        //隱藏返回按鈕中的文字
        navigationItem.backBarButtonItem = UIBarButtonItem(customView: btn)
        navigationController?.pushViewController(vc, animated: animated)
    }
複製程式碼

可選擇思路:

  • 自定義navigationController,重寫push方法,在push方法中做類似的事情
  • viewController中做文章,重寫返回按鈕

LargeTitle

largeTitle.png

在iOS11導航欄多了一個LargeTitleView,預設是不開啟的,專門顯示大字標題用的,整個導航欄的高度達到了96p,這不包括狀態列的高度,也就是說,整個app頂部高度達到了116p,其中statusbar=20,title=44,largetitle=52,不過預設是64p;當然,iPhoneX的高度會更高點,如果不顯示大字標題,頂部的高度也達到了88statusbar=44,title=44,如果顯示大字標題,則高度變成了140statusbar=44,title=44,largetitle=52,也就是說,iPhoneX的劉海高度為24p,大字標題如下圖:

largeTitle1.png

largeTitle2.png

具體設定: 在UI navigation bar中新增了一個BOOL屬性prefersLargeTitles,將該屬性設定為ture,navigation bar就會在整個APP中顯示大標題,如果想要在控制不同頁面大標題的顯示,可以通過設定當前頁面的navigationItemlargeTitleDisplayMode屬性;

navigationItem.largeTitleDisplayMode 
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {  
/// 自動模式依賴上一個 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 針對當前 item 總是啟用大標題特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never 
UINavigationItemLargeTitleDisplayModeNever,
}
複製程式碼

3. Navigation 整合 UISearchController

把你的UISearchController賦值給navigationItem,就可以實現將UISearchController整合到NavigationBar

navigationItem.searchController  //iOS 11 新增屬性
navigationItem.hidesSearchBarWhenScrolling //決定滑動的時候是否隱藏搜尋框;iOS 11 新增屬性
複製程式碼

4. 導航欄的邊距變化

  1. 如果只是設定了titleView,沒有設定barbutton,把titleview的寬度設定為螢幕寬度,則titleview距離螢幕的邊距

    • iOS11之前,在iPhone6p上是20p,在iPhone6p之前是16p
    • iOS11之後,在iPhone6p上是12p,在iPhone6p之前是8p
  2. 如果只是設定了barbutton,沒有設定titleview

    • 在iOS11裡,barButton距離螢幕的邊距是20p16p
    • 在iOS11之前,barButton距離螢幕的邊距也是20p16p
  3. 如果同時設定了titleViewbarButton

    • 則在iOS11之前,titleview和barbutton之間的間距是6p
    • 在iOS11上titleview和barbutton之間無間距,如下圖:

titleView.png

5. UINavigationController和滾動互動

滾動的時候,以下互動操作都是由UINavigationController負責調動的:

  1. UIsearchController搜尋框效果更新
  2. 大標題效果的控制
  3. Rubber banding效果 //當你開始往下拉,大標題會變大來回應那個滾輪 所以,如果你使用navigation bar,組裝一些整個push和pop體驗,你不會得到searchController的整合、大標題的控制更新和Rubber banding效果,因為這些都是由UINavigationController控制的。

6. UIToolbar and UINavigationBar— Layout

在 iOS 11 中,當蘋果進行所有這些新特性時,也進行了其他的優化,針對 UIToolbar 和 UINavigaBar 做了新的自動佈局擴充套件支援,自定義的bar button items、自定義的title都可以通過layout來表示尺寸。 需要注意的是,你的constraints需要在view內部設定,所以如果你有一個自定義的標題檢視,你需要確保任何約束只依賴於標題檢視及其任何子檢視。當你使用自動佈局,系統假設你知道你在做什麼。

7. Avoiding Zero-Sized Custom Views

自定義檢視的size為0是因為你有一些模糊的約束佈局。要避免檢視尺寸為0,可以從以下方面做:

  • UINavigationBar 和 UIToolbar 提供位置
  • 開發者則必須提供檢視的size,有三種方式:
    • 對寬度和高度的約束;
    • 實現 intrinsicContentSize;
    • 通過約束關聯你的子檢視;

二、UITabBar

iPhoneX不止多了劉海,底部還有一個半形的矩形,使得tabbar多出來了34p的高度,不過不管navigationBar還是tabbar一般系統都會自動適配safeArea。

tabbar.png

三、管理margins 和 insets

1. layout margins

基於約束的Auto Layout,使我們搭建能夠動態響應內部和外部變化的使用者介面。Auto Layout為每一個view都定義了marginmargin指的是控制元件顯示內容部分的邊緣和控制元件邊緣的距離。 可以用layoutMargins或者layoutMarginsGuide屬性獲得viewmargin,margin是檢視內部的一部分。layoutMargins允許獲取或者設定UIEdgeInsets結構的marginlayoutMarginsGuide則獲取到只讀的UILayoutGuide物件。

在iOS11新增了一個屬性:directional layout margins,該屬性是NSDirectionalEdgeInsets結構體型別的屬性:

typedef struct NSDirectionalEdgeInsets {  
    CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
複製程式碼

layoutMarginsUIEdgeInsets結構體型別的屬性:

typedef struct UIEdgeInsets {  
CGFloat top, left, bottom, right;
} UIEdgeInsets;
複製程式碼

從上面兩種結構體的對比可以看出,NSDirectionalEdgeInsets 屬性用leadingtrailing 取代了之前的 leftrightdirectional layout margins屬性的說明如下:

directionalLayoutMargins.leading is used on the left when the user interface direction is LTR and on the right for RTL.
Vice versa for directionalLayoutMargins.trailing.
複製程式碼

例子:當你設定了trailing = 30;當在一個right to left 語言下trailing的值會被設定在view的左邊,可以通過layoutMarginleft屬性讀出該值。如下圖所示:

margin.png
還有其他一些更新。自從引入layout margins,當將一個view新增到viewController時,viewController會修復viewlayoutMarginsUIKit定義的一個值,這些調整對外是封閉的。從iOS11開始,這些不再是一個固定的值,它們實際是最小值,你可以改變viewlayoutMargins為任意一個更大的值。而且,viewController新增了一個屬性:viewRespectsSystemMinimumLayoutMargins,如果你設定該屬性為"false",你就可以改變你的layoutMargins為任意你想設定的值,包括0,如下圖所示:

margin2.png

2. 安全區域(Safe Area)

iOS 7 開始,在 UIViewController中引入的 topLayoutGuidebottomLayoutGuide 在 iOS 11 中被廢棄了!取而代之的就是safeArea的概念,safeArea是描述你的檢視部分不被任何內容遮擋的方法。 它提供兩種方式:safeAreaInsetssafeAreaLayoutGuide來提供給你safeArea的參照值,即 insets 或者 layout guide。 safeArea區域如圖所示:

safe-area.png

如果有一個自定義的viewController,你可能要新增你自己的bars,增加safeAreaInsets的值,可以通過一個新的屬性:addtionalSafeAreaInsets來改變safeAreaInsets的值,當你的viewController改變了它的safeAreaInsets值時,有兩種方式獲取到回撥:

UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
複製程式碼

四、UIScrollView and UITableView的新特性

1. UIScrollView (棄用automaticallyAdjustsScrollViewInsets)

如果有一些文字位於UI滾動檢視的內部,幷包含在導航控制器中,一般navigationContollers會傳入一個contentInset給其最頂層的viewController的scrollView,在iOS11中進行了一個很大的改變,不再通過scrollView的contentInset屬性了,而是新增了一個屬性:adjustedContentInset,通過下面兩種圖的對比,能夠表示adjustContentInset表示的區域:

adjustedContentInset1.png
adjustedContentInset2.png
取消掉原有的automaticallyAdjustsScrollViewInsets屬性,新增的contentInsetAdjustmentBehavior屬性用來配置adjustedContentInset的行為,該結構體有以下幾種型別:

typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {  
    UIScrollViewContentInsetAdjustmentAutomatic, 
    UIScrollViewContentInsetAdjustmentScrollableAxes,
    UIScrollViewContentInsetAdjustmentNever,
    UIScrollViewContentInsetAdjustmentAlways,
}

@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset;

//adjustedContentInset值被改變的delegate
- (void)adjustedContentInsetDidChange; 
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView;
複製程式碼

針對automaticallyAdjustsScrollViewInsets屬性被棄用,比較簡單的適配方法

if (@available(iOS 11.0, *)) {
    self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
    self.automaticallyAdjustsScrollViewInsets = NO;
}
複製程式碼

2. Table Views :在iOS 11中預設啟用Self-Sizing

這個應該是UITableView最大的改變。我們知道在iOS8引入Self-Sizing 之後,我們可以通過實現estimatedRowHeight相關的屬性來展示動態的內容,實現了estimatedRowHeight屬性後,得到的初始contenSize是個估算值,是通過estimatedRowHeight x cell的個數得到的,並不是最終的contenSizetableView不會一次性計算所有的cell的高度了,只會計算當前螢幕能夠顯示的cell個數再加上幾個,滑動時,tableView不停地得到新的cell,更新自己的contenSize,在滑到最後的時候,會得到正確的contenSize。建立tableView到顯示出來的過程中,contentSize的計算過程如下圖:

contentSize.png
Self-Sizing在iOS11下是預設開啟的,Headers, footers, and cells都預設開啟Self-Sizing,所有estimated 高度預設值從iOS11之前的 0 改變為UITableViewAutomaticDimension 由於Self-Sizing的緣故,contentSizecontentOffset的值在滾動的過程中會一直產生變化,所以如果目前的專案你還沒有使用到Self-Sizing,contentSizecontentOffset有關的動畫可能會有問題。

iOS 11中如果不實現-tableView: viewForFooterInSection:-tableView: viewForHeaderInSection:,那麼-tableView: heightForHeaderInSection:- tableView: heightForFooterInSection:不會被呼叫。

iOS11下不想使用Self-Sizing的話,可以通過以下方式關閉:

self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
複製程式碼

3. Table Views:separatorInset 擴充套件

iOS 7 引入separatorInset屬性,用以設定 cell 的分割線邊距,在 iOS 11 中對其進行了擴充套件。可以通過新增的UITableViewSeparatorInsetReference列舉型別的separatorInsetReference屬性來設定separatorInset屬性的參照值。

typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {  
    UITableViewSeparatorInsetFromCellEdges,   //預設值,表示separatorInset是從cell的邊緣的偏移量
    UITableViewSeparatorInsetFromAutomaticInsets  //表示separatorInset屬性值是從一個insets的偏移量
}
複製程式碼

下圖清晰的展示了這兩種參照值的區別:

inset.png

4. Table Views 和 Safe Area

有以下幾點需要注意:

  1. separatorInset 被自動地關聯到 safe area insets,因此,預設情況下,表檢視的整個內容避免了其根檢視控制器的安全區域的插入。
  2. UITableviewCellUITableViewHeaderFooterViewcontentview 在安全區域內;因此你應該始終在 contentview 中使用add-subviews操作。
  3. 所有的 headersfooters 都應該使用UITableViewHeaderFooterView,包括 table headersfooterssection headersfooters

5. 滑動操作(Swipe Actions)

在iOS8之後,蘋果官方增加了UITableVIew的右滑操作介面,即新增了一個代理方法(tableView: editActionsForRowAtIndexPath:)和一個類(UITableViewRowAction),代理方法返回的是一個陣列,我們可以在這個代理方法中定義所需要的操作按鈕(刪除、置頂等),這些按鈕的類就是UITableViewRowAction。這個類只能定義按鈕的顯示文字、背景色、和按鈕事件。並且返回陣列的第一個元素在UITableViewCell的最右側顯示,最後一個元素在最左側顯示。從iOS 11開始有了一些改變,首先是可以給這些按鈕新增圖片了,然後是如果實現了以下兩個iOS 11新增的代理方法,將會取代(tableView: editActionsForRowAtIndexPath:)代理方法:

// Swipe actions
// These methods supersede -editActionsForRowAtIndexPath: if implemented
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
複製程式碼

這兩個代理方法返回的是UISwipeActionsConfiguration型別的物件,建立該物件及賦值可看下面的程式碼片段:

- ( UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
    //刪除
    UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"delete" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
        [self.titleArr removeObjectAtIndex:indexPath.row];
        completionHandler (YES);
    }];
    deleteRowAction.image = [UIImage imageNamed:@"icon_del"];
    deleteRowAction.backgroundColor = [UIColor blueColor];

    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction]];
    return config;
}
複製程式碼

建立UIContextualAction物件時,UIContextualActionStyle有兩種型別,如果是置頂、已讀等按鈕就使用UIContextualActionStyleNormal型別,delete操作按鈕可使用UIContextualActionStyleDestructive型別,當使用該型別時,如果是右滑操作,一直向右滑動某個cell,會直接執行刪除操作,不用再點選刪除按鈕,這也是一個好玩的更新。

typedef NS_ENUM(NSInteger, UIContextualActionStyle) {
    UIContextualActionStyleNormal,
    UIContextualActionStyleDestructive
} NS_SWIFT_NAME(UIContextualAction.Style)
複製程式碼

滑動操作這裡還有一個需要注意的是,當cell高度較小時,會只顯示image,不顯示title,當cell高度夠大時,會同時顯示imagetitle。我寫demo測試的時候,因為每個cell的高度都較小,所以只顯示image,然後我增加cell的高度後,就可以同時顯示imagetitle了。見下圖對比:

swipe.png

參考連結

你可能需要為你的APP適配iOS11

App介面適配iOS11(包括iPhoneX的奇葩尺寸)

相關文章