iOS11、iPhoneX、Xcode9 的注意點彙總

這裏寫圖片描述

參考文章:
WWDC 2017 session204: Updating Your App for iOS 11
Apple 官方文檔: Human Interface Guidelines
iPhone X 中文官方適配文檔
你可能需要爲你的 APP 適配 iOS11
iOS11 導航欄按鈕位置問題的解決
iOS11 遇到的坑及解決方法
適配 iOS11&iPhoneX 的一些坑
iOS11 & iPhoneX 適配 & Xcode9 打包注意事項
App 界面適配 iOS11(包括 iPhoneX 的奇葩尺寸)
簡書 App 適配 iOS 11
Xcode9 打包報錯問題解決
iOS11 訪問相冊權限變更問題
The Ultimate Guide To iPhone Resolutions
iPhoneX狀態條的隱藏與顯示

首先,請注意工程中依賴的第三方代碼(framework, library或是源碼),需要留意其適配 iOS11、iPhoneX 的更新。
自己的代碼,可以參照下面整理的適配。

一. 先來熱個身,^_^

1. 每次蘋果發佈新的系統,我們都要注意下新系統相關宏的支持、廢棄 API 的替換:

#if (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR)
    const BOOL IOS11_OR_LATER = ( [[[UIDevice currentDevice] systemVersion] compare:@"11.0" options:NSNumericSearch] != NSOrderedAscending );
    const BOOL IOS10_OR_EARLIER = !IOS11_OR_LATER;
#else 
    const BOOL IOS11_OR_LATER = NO;
    const BOOL IOS10_OR_EARLIER = NO;
#endif

2. 每次蘋果發佈新的設備,我們也要注意下新設備相關宏的支持:

更多新設備信息詳見: Github-iOS-getClientInfo

@"iPhone10,1" : @"iPhone 8 國行/日版"
@"iPhone10,4" : @"iPhone 8 美版(Global)"
@"iPhone10,2" : @"iPhone 8 Plus 美版(Global)"
@"iPhone10,5" : @"iPhone 8 Plus 美版(Global)"
@"iPhone10,3" : @"iPhone X 國行/日版"
@"iPhone10,6" : @"iPhone X 美版(Global)"
#if (TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR)
    const BOOL IS_SCREEN_55_INCH = ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO);
    const BOOL IS_SCREEN_58_INCH = ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO);
#else
    const BOOL IS_SCREEN_55_INCH = NO;
    const BOOL IS_SCREEN_58_INCH = NO;
#endif

下圖是 iPhone 常見型號的屏幕相關對比:
更詳細的信息可查閱:The Ultimate Guide To iPhone Resolutions
圖片

iPhone 8 與 iPhone X 的尺寸對比:
圖片

二. 狀態欄

iPhone X狀態條由20px變成了44px,UITabBar由49px變成了83px。設置佈局時y直接寫成64的就要根據機型設置。可以設置宏

#define Device_Is_iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)

然後再設置。

三. 導航欄

1. 導航欄高度的變化

iOS11之前導航欄默認高度爲64pt(這裏高度指statusBar + NavigationBar),iOS11之後如果設置了prefersLargeTitles = YES則爲96pt,默認情況下還是64pt,但在iPhoneX上由於劉海的出現statusBar由以前的20pt變成了44pt,所以iPhoneX上高度變爲88pt,如果項目裏隱藏了導航欄加了自定義按鈕之類的,這裏需要注意適配一下。

2. 導航欄圖層及對titleView佈局的影響

iOS11之前導航欄的title是添加在UINavigationItemView上面,而navigationBarButton則直接添加在UINavigationBar上面,如果設置了titleView,則titleView也是直接添加在UINavigationBar上面。iOS11之後,大概因爲largeTitle的原因,視圖層級發生了變化,如果沒有給titleView賦值,則titleView會直接添加在_UINavigationBarContentView上面,如果賦值了titleView,則會把titleView添加在_UITAMICAdaptorView上,而navigationBarButton被加在了_UIButtonBarStackView上,然後他們都被加在了_UINavigationBarContentView上,如圖:
圖片
所以如果你的項目是自定義的navigationBar,那麼在iOS11上運行就可能出現佈局錯亂的bug,解決辦法是重寫UINavigationBar的layoutSubviews方法,調整佈局,上代碼:

- (void)layoutSubviews {
    [super layoutSubviews];

    // 注意導航欄及狀態欄高度適配
    self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), naviBarHeight);
    for (UIView *view in self.subviews) {
        if([NSStringFromClass([view class]) containsString:@"Background"]) {
            view.frame = self.bounds;
        }
        else if ([NSStringFromClass([view class]) containsString:@"ContentView"]) {
            CGRect frame = view.frame;
            frame.origin.y = statusBarHeight;
            frame.size.height = self.bounds.size.height - frame.origin.y;
            view.frame = frame;
        }
    }
}

再補充一點,看了簡書App適配iOS11發現titleView支持autolayout,這要求titleView必須是能夠自撐開的或實現了- intrinsicContentSize方法

- (CGSize)intrinsicContentSize {
    return UILayoutFittingExpandedSize;
}

3. 控制大標題的顯示

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

navigationItem.largeTitleDisplayMode 

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

4. Navigation 集成 UISearchController

把你的UISearchController賦值給navigationItem,就可以實現將UISearchController集成到Navigation

navigationItem.searchController  //iOS 11 新增屬性
navigationItem.hidesSearchBarWhenScrolling //決定滑動的時候是否隱藏搜索框;iOS 11 新增屬性

使用Xcode9 編譯後發現原生的搜索欄樣式發生改變,如下圖,右邊爲 iOS 11 樣式,搜索區域高度變大,字體變大。

圖片

5. UINavigationController 和滾動交互

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

UIsearchController搜索框效果更新
大標題效果的控制
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;

    • 通過約束關聯你的子視圖;

8. 導航欄按鈕的位置問題

在 iOS7 之後,我們在設置 UINavigationItem 的 leftBarButtonItem,rightBarButtonItem 的時候都會造成位置的偏移,雖然不大,但是跟 UI 的設計或者國人的習慣有點區別,當然也有很好的解決方案,多添加一個消極的寬度爲負值的 UIBarButtonItem

+(UIBarButtonItem *)fixedSpaceWithWidth:(CGFloat)width {

    UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    fixedSpace.width = width;
    return fixedSpace;
}

在我們添加導航欄按鈕的時候
我們使用就可以滿足將按鈕位置調整的需求

[self.navigationItem setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];

但是這些在iOS 11中都無效了!!!!
但是這些在iOS 11中都無效了!!!!
但是這些在iOS 11中都無效了!!!!
重要的事情說3遍。

iOS 11改動相當大的就是導航欄的部分,在原來的已經複雜的不要的圖層中又新增了新的圖層!
圖片
是的你沒有看做,_UINavigationBarContentView和_UIButtonBarStackView和_UITAMICAdaptorView
而我們之前的leftBarButtonItem什麼的現在都在UIButtonBarStackView中了.更無語的是這些

<_UIButtonBarStackView: 0x7ff988074290; frame = (12 0; 48 44); layer = <CALayer: 0x60000042bc80>>
Printing description of $11:
<UIView: 0x7ff9880764a0; frame = (0 22; 8 0); layer = <CALayer: 0x60000042b7c0>>
Printing description of $12:
<_UITAMICAdaptorView: 0x7ff988076790; frame = (8 2; 40 40); autoresizesSubviews = NO; layer = <CALayer: 0x60000042b8a0>>

我們可以看到一個_UIButtonBarStackView佔掉了12個像素的左邊約束,_UITAMICAdaptorView又佔據了8個像素的左邊約束,所以說我們很無語的就被佔據了20px,更可氣的是,都是私有對象,不容易修改!

於是還是老套路,我們設置負值來調整約束,結果卻失敗了,無效…
迫於無奈,我們只能想新的辦法。

  • 放棄UIBarButtonItem,放棄UINavigationBar,使用自定義視圖代替
  • 在UINavigationBar中使用添加視圖的方式,固定位置固定大小添加按鈕
  • UIBarButtonItem.customView 設置偏移(比如按鈕設置圖片偏移 視圖設置tranform等)
  • 修改UIBarButtonItem圖層結構(刪除圖層,或者修改約束)

當然,完全的使用自定義視圖代替原生的UINavigationBar和UIBarButtonItem,這裏我也不需要說明了.就是自定義視圖蠻,肯定都能解決

使用addSubview:添加,之後remove什麼的雖然可以,但是這個也不是我想要的

至於這是偏移,結果也嗯慘淡,無效.我嘗試了設置旋轉都可以,但是設置位置左移就失效了.很無語

爲什麼非要大動代碼呢?在iOS 11之前,我們的項目絕大部分都是使用UINavigationBar和UIBarButtonItem,也就是系統的來管理,現在如果因爲一個偏移問題,我們就要修改過多代碼,豈不是很麻煩?
能否有較小代價實現?
答案是有的。

我們可能會做這樣的一個分類

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];
    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        [self sx_setLeftBarButtonItem:nil];
        [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        [self sx_setRightBarButtonItem:nil];
        [self setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];
    }else {
        [self setRightBarButtonItems:nil];
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}
@end

在我們iOS11之前,我們使用這樣的一個分類來擴展,
使得我們在vc中就能這樣使用

self.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithTarget:self action:@selector(sx_pressBack:) image:[UIImage imageNamed:@"nav_back"]];

就能調整好我們的按鈕位置

那麼能不能不懂這些代碼也滿足iOS 11呢?

那麼只有在加一點點東西了,在分類中

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            //如果調整,在這裏實現,這樣就能達到不影響代碼的效果
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

在什麼地方寫我們都能想明白,接下來是怎麼寫的問題了
我的思路是既然他是一個customView,那麼我能否擴展這個customView呢?
我們原來將一個按鈕直接用作customView,比如這樣

[[UIBarButtonItem alloc] initWithCustomView:button];

但是現在我想的是按鈕添加在一個我們定義的view中,view作爲customView
這樣view作爲一個位置調整的視圖,就可以相對自由的定義了

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = leftBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionLeft];//說明這個view需要調整的是左邊
            [self setLeftBarButtonItems:nil];
            [self sx_setLeftBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

那麼這個view我們也能幹些事情了

typedef NS_ENUM(NSInteger, SXBarViewPosition) {
    SXBarViewPositionLeft,
    SXBarViewPositionRight
};

@interface BarView : UIView
@property (nonatomic, assign) SXBarViewPosition position;
@property (nonatomic, assign) BOOL applied;
@end

@implementation BarView

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.applied || [[[UIDevice currentDevice] systemVersion] floatValue]  < 11) return;
    UIView *view = self;
    while (![view isKindOfClass:UINavigationBar.class] && view.superview) {
        view = [view superview];
        if ([view isKindOfClass:UIStackView.class] && view.superview) {
            if (self.position == SXBarViewPositionLeft) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] &&
                     constraint.firstAttribute == NSLayoutAttributeTrailing) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeLeading
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeLeading
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            } else if (self.position == SXBarViewPositionRight) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] &&
                     constraint.firstAttribute == NSLayoutAttributeLeading) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeTrailing
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeTrailing
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            }
            break;
        }
    }
}

@end

代碼其實不復雜,就是遍歷view的父視圖,當其實UIStackView的時候,我們修改其左右約束,但是僅僅修改的話會造成約束衝突,所以我們還需要提前移除約束衝突的左右約束(如果擔心影響問題,不移除沒有關係,僅僅是編譯器會報約束衝突log,代碼潔癖的話會感覺不舒服)

於是在原來的分類中稍作擴展,我們的新的分類就完成了

#import "UINavigationItem+SXFixSpace.h"
#import "NSObject+SXRuntime.h"
#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, SXBarViewPosition) {
    SXBarViewPositionLeft,
    SXBarViewPositionRight
};

@interface BarView : UIView
@property (nonatomic, assign) SXBarViewPosition position;
@property (nonatomic, assign) BOOL applied;
@end

@implementation BarView

- (void)layoutSubviews {
    [super layoutSubviews];
    if (self.applied || [[[UIDevice currentDevice] systemVersion] floatValue]  < 11) return;
    UIView *view = self;
    while (![view isKindOfClass:UINavigationBar.class] && view.superview) {
        view = [view superview];
        if ([view isKindOfClass:UIStackView.class] && view.superview) {
            if (self.position == SXBarViewPositionLeft) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && 
                    constraint.firstAttribute == NSLayoutAttributeTrailing) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeLeading
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeLeading
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            } else if (self.position == SXBarViewPositionRight) {
                for (NSLayoutConstraint *constraint in view.superview.constraints) {
                    if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && 
                    constraint.firstAttribute == NSLayoutAttributeLeading) {
                        [view.superview removeConstraint:constraint];
                    }
                }
                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view
                                                                           attribute:NSLayoutAttributeTrailing
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:view.superview
                                                                           attribute:NSLayoutAttributeTrailing
                                                                          multiplier:1.0
                                                                            constant:0]];
                self.applied = YES;
            }
            break;
        }
    }
}

@end

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];
    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem{
    if (leftBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = leftBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionLeft];
            [self setLeftBarButtonItems:nil];
            [self sx_setLeftBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        }else {
            [self sx_setLeftBarButtonItem:nil];
            [self setLeftBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], leftBarButtonItem]];
        }
    }else {
        [self setLeftBarButtonItems:nil];
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 11) {
            UIView *customView = rightBarButtonItem.customView;
            BarView *barView = [[BarView alloc]initWithFrame:customView.bounds];
            [barView addSubview:customView];
            customView.center = barView.center;
            [barView setPosition:SXBarViewPositionRight];
            [self setRightBarButtonItems:nil];
            [self sx_setRightBarButtonItem:[[UIBarButtonItem alloc]initWithCustomView:barView]];
        } else {
            [self sx_setRightBarButtonItem:nil];
            [self setRightBarButtonItems:@[[UIBarButtonItem fixedSpaceWithWidth:-20], rightBarButtonItem]];
        }
    }else {
        [self setRightBarButtonItems:nil];
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}

@end

使用前:
圖片

使用後:
圖片

我不需要需改任何界面上的代碼,在iOS 11下解決了導航欄按鈕位置問題
當然你也能在做擴展,是偏移多少,修改約束的值即可
上面部分代碼省略,完整demo請移步下載

使用中可能會遇到的問題及解決方法:
1. 某一個界面在push一個新界面之後再返回回來之後位置就還原了
解決方案其實很簡單,只要將設置leftItem的方法寫在viewWillAppear中即可,這樣即可保證約束不會被系統重置
2. demo中的刪除約束的判斷僅僅是我個人項目中的判斷,每個開發者的項目因爲各種因素可能會有不同的影響,大家可以根據項目自行判斷需要刪除的約束條件,亦或者是不刪除約束也是可以的

上面的問題另外一個解決方法:
使用layoutMargins這個屬性
圖片
我們遍歷圖層大致可以看到這樣的

<_UINavigationBarContentView: 0x7fc141607250; frame = (0 0; 414 44); layer = <CALayer: 0x608000038cc0>>

這個UINavigationBarContentView平鋪在導航欄中作爲iOS11的各個按鈕的父視圖,該視圖的所有的子視圖都會有一個layoutMargins被佔用,也就是系統調整的佔位,我們只要把這個置空就行了.那樣的話該視圖下的所有的子視圖的空間就會變成我們想要的那樣,當然爲了保險起見,該視圖的父視圖也就是bar的layoutMargins也置空,這樣 整個bar就會跟一個普通視圖一樣了 左右的佔位約束就不存在了

於是就出現了這樣的代碼

@implementation UINavigationBar (SXFixSpace)
+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethodWithOriginSel:@selector(layoutSubviews)
                                     swizzledSel:@selector(sx_layoutSubviews)];
    });
}

-(void)sx_layoutSubviews{
    [self sx_layoutSubviews];

    if (deviceVersion >= 11) {
        self.layoutMargins = UIEdgeInsetsZero;
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsZero;//可修正iOS11之後的偏移
            }
        }
    }
}

@end

是的,這一次的修正方式何其的輕鬆,之前饒了太多的彎路….

於是在結合iOS11之前的特性,和並出新的解決導航欄按鈕問題的新的解決方案,
這一次,修正的更加徹底
相較於上一次的優勢,
1.可以使用itmes方式設置多個按鈕
2.可以不寫在viewWillAppear中也可以滿足push和pop不更改約束的問題
3.不對約束進行修改,修改的是layoutMargins,使其默認的20變成0,這樣不影響導航欄中其他視圖的約束衝突問題
4.代碼量不重,和之前不通,這次僅僅是調整layoutMargins,不需要爲了修改約束等再添加圖層等,具體可以看我之前的,比較寫法差異
5.最後代碼也更加簡潔.

@implementation UINavigationBar (SXFixSpace)

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethodWithOriginSel:@selector(layoutSubviews)
                                     swizzledSel:@selector(sx_layoutSubviews)];
    });
}

-(void)sx_layoutSubviews{
    [self sx_layoutSubviews];

    if (deviceVersion >= 11) {
        self.layoutMargins = UIEdgeInsetsZero;
        CGFloat space = sx_tempFixSpace !=0 ? sx_tempFixSpace : sx_defaultFixSpace;
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsMake(0, space, 0, space);//可修正iOS11之後的偏移
                break;
            }
        }
    }
}

@end

@implementation UINavigationItem (SXFixSpace)

+(void)load {
    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItem:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItem:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setLeftBarButtonItems:)
                                 swizzledSel:@selector(sx_setLeftBarButtonItems:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItem:)
                                 swizzledSel:@selector(sx_setRightBarButtonItem:)];

    [self swizzleInstanceMethodWithOriginSel:@selector(setRightBarButtonItems:)
                                 swizzledSel:@selector(sx_setRightBarButtonItems:)];
}

-(void)sx_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem {
    if (leftBarButtonItem.customView) {
        if (deviceVersion >= 11) {
            sx_tempFixSpace = 0;
            [self sx_setLeftBarButtonItem:leftBarButtonItem];
        } else {
            [self setLeftBarButtonItems:@[leftBarButtonItem]];
        }
    } else {
        sx_tempFixSpace = 20;
        [self sx_setLeftBarButtonItem:leftBarButtonItem];
    }
}

-(void)sx_setLeftBarButtonItems:(NSArray<UIBarButtonItem *> *)leftBarButtonItems {
    NSMutableArray *items = [NSMutableArray arrayWithObject:[self fixedSpaceWithWidth:sx_defaultFixSpace-20]];//可修正iOS11之前的偏移
    [items addObjectsFromArray:leftBarButtonItems];
    [self sx_setLeftBarButtonItems:items];
}

-(void)sx_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem{
    if (rightBarButtonItem.customView) {
        if (deviceVersion >= 11) {
            sx_tempFixSpace = 0;
            [self sx_setRightBarButtonItem:rightBarButtonItem];
        } else {
            [self setRightBarButtonItems:@[rightBarButtonItem]];
        }
    } else {
        sx_tempFixSpace = 20;
        [self sx_setRightBarButtonItem:rightBarButtonItem];
    }
}

-(void)sx_setRightBarButtonItems:(NSArray<UIBarButtonItem *> *)rightBarButtonItems{
    NSMutableArray *items = [NSMutableArray arrayWithObject:[self fixedSpaceWithWidth:sx_defaultFixSpace-20]];//可修正iOS11之前的偏移
    [items addObjectsFromArray:rightBarButtonItems];
    [self sx_setRightBarButtonItems:items];
}

-(UIBarButtonItem *)fixedSpaceWithWidth:(CGFloat)width {
    UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                                                                               target:nil
                                                                               action:nil];
    fixedSpace.width = width;
    return fixedSpace;
}

@end

效果和之前的解決方案几乎一樣,只能說這次是換了思路實現的
圖片
可以很明顯的看到間距不是20,至於是多少?
圖片
我用宏定義的方式設置的,你也可以自定義,或者使用其他的方式確定其大小。

layoutMargins解決方法的demo地址

9. 導航欄的邊距變化

在iOS11對導航欄裏面的item的邊距也做了調整:
(1)如果只是設置了titleView,沒有設置barbutton,把titleview的寬度設置爲屏幕寬度,則titleview距離屏幕的邊距,iOS11之前,在iPhone6p上是20p,在iPhone6p之前是16p;iOS11之後,在iPhone6p上是12p,在iPhone6p之前是8p。

(2)如果只是設置了barbutton,沒有設置titleview,則在iOS11裏,barButton距離屏幕的邊距是20p和16p;在iOS11之前,barButton距離屏幕的邊距也是20p和16p。

(3)如果同時設置了titleView和barButton,則在iOS11之前,titleview和barbutton之間的間距是6p,在iOS11上titleview和barbutton之間無間距,如下圖:
圖片
圖片

10. 導航欄返回按鈕

之前的代碼通過下面的方式自定義返回按鈕(可以隱藏返回按鈕的標題)

UIImage *backButtonImage = [[UIImage imageNamed:@"icon_tabbar_back"]
    resizableImageWithCapInsets:UIEdgeInsetsMake(0, 18, 0, 0)];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:backButtonImage
                                                  forState:UIControlStateNormal
                                                barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
                                                     forBarMetrics:UIBarMetricsDefault];

iOS 11 中setBackButtonTitlePositionAdjustment:UIOffsetMake沒法把按鈕移出navigation bar。
解決方法是設置navigationController的backIndicatorImage和backIndicatorTransitionMaskImage

UIImage *backButtonImage = [[UIImage imageNamed:@"icon_tabbar_back"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
self.navigationBar.backIndicatorImage = backButtonImage;
self.navigationBar.backIndicatorTransitionMaskImage = backButtonImage;

iOS 11 想通過setBackButtonTitlePositionAdjustment:UIOffsetMake隱藏返回按鈕文字,可以像下面這樣做適配:

   // 隱藏導航欄返回按鈕文字
    if (@available(iOS 11, *)) {
        [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(-200, 0)
                                                             forBarMetrics:UIBarMetricsDefault];
    } else {
        [[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
                                                             forBarMetrics:UIBarMetricsDefault];
    }

四. 管理 margins 和 insets

1. layout margins

基於約束的Auto Layout,使我們搭建能夠動態響應內部和外部變化的用戶界面。Auto Layout爲每一個view都定義了marginmargin指的是控件顯示內容部分的邊緣和控件邊緣的距離。
可以用layoutMargins或者layoutMarginsGuide屬性獲得view的margin, 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屬性用 leading 和 trailing 取代了之前的 left 和 right。

directional 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的左邊,可以通過layoutMargin的left屬性讀出該值。如下圖所示:
圖片
還有其他一些更新。自從引入layout margins,當將一個view添加到viewController時,viewController會修復view的layoutMargins爲UIKit定義的一個值,這些調整對外是封閉的。從iOS11開始,這些不再是一個固定的值,它們實際是最小值,你可以改變view的layoutMargins爲任意一個更大的值。而且,viewController新增了一個屬性:viewRespectsSystemMinimumLayoutMargins,如果你設置該屬性爲”false”,你就可以改變你的layoutMargins爲任意你想設置的值,包括0,如下圖所示:
圖片

五. 安全區域(Safe Area)

在 iOS 11 上運行 tableView 向下偏移 64px 或者 20px,因爲 iOS 11 廢棄了 automaticallyAdjustsScrollViewInsets,而是給 UIScrollView 增加了 contentInsetAdjustmentBehavior 屬性。避免這個坑的方法是要判斷

if (@available(iOS 11.0, *)) {
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
#define  adjustsScrollViewInsets(scrollView)\
do {\
_Pragma("clang diagnostic push")\
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")\
if ([scrollView respondsToSelector:NSSelectorFromString(@"setContentInsetAdjustmentBehavior:")]) {\
    NSMethodSignature *signature = [UIScrollView instanceMethodSignatureForSelector:@selector(setContentInsetAdjustmentBehavior:)];\
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];\
    NSInteger argument = 2;\
    invocation.target = scrollView;\
    invocation.selector = @selector(setContentInsetAdjustmentBehavior:);\
    [invocation setArgument:&argument atIndex:2];\
    [invocation retainArguments];\
    [invocation invoke];\
}\
_Pragma("clang diagnostic pop")\
} while (0)

還有的發現某些界面tableView的sectionHeader、sectionFooter高度與設置不符的問題,在iOS11中如果不實現 -tableView: viewForHeaderInSection:和-tableView: viewForFooterInSection: ,則-tableView: heightForHeaderInSection:和- tableView: heightForFooterInSection:不會被調用,導致它們都變成了默認高度,這是因爲tableView在iOS11默認使用Self-Sizing,tableView的estimatedRowHeight、estimatedSectionHeaderHeight、 estimatedSectionFooterHeight三個高度估算屬性由默認的0變成了UITableViewAutomaticDimension,解決辦法簡單粗暴,就是實現對應方法或把這三個屬性設爲0。

如果你使用了Masonry,那麼你需要適配safeArea

if (@available(iOS 11.0, *)) {
    make.edges.equalTo()(self.view.safeAreaInsets)
} else {
    make.edges.equalTo()(self.view)
}

如下圖:照片應用程序
圖片
從iOS 7以來,我們在整個操作系統中都有這些半透明的bars,蘋果鼓勵我們通過這些bars繪製內容,我們是通過viewController 的edgesForExtendedLayout屬性來做這些的。
iOS 7 開始,在UIViewController中引入的topLayoutGuidebottomLayoutGuide在 iOS 11 中被廢棄了!取而代之的就是safeArea的概念,safeArea是描述你的視圖部分不被任何內容遮擋的方法。 它提供兩種方式:safeAreaInsetssafeAreaLayoutGuide來提供給你safeArea的參照值,即 insets或者layout guidesafeArea區域如圖所示:
圖片
如果有一個自定義的viewController,你可能要添加你自己的bars,增加safeAreaInsets的值,可以通過一個新的屬性:addtionalSafeAreaInsets來改變safeAreaInsets的值,當你的viewController改變了它的safeAreaInsets值時,有兩種方式獲取到回調:

UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()

六. UIScrollView

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

圖片
新增的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;

七. UITableView

1. 在iOS 11中默認啓用Self-Sizing

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

Self-Sizing在iOS11下是默認開啓的,Headers, footers, and cells都默認開啓Self-Sizing,所有estimated 高度默認值從iOS11之前的 0 改變爲UITableViewAutomaticDimension:

@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is UITableViewAutomaticDimension, set to 0 to disable

如果目前項目中沒有使用 estimateRowHeight 屬性,在 iOS11 的環境下就要注意了,因爲開啓 Self-Sizing 之後,tableView 是使用 estimateRowHeight 屬性的,這樣就會造成 contentSize 和 contentOffset 值的變化,如果是有動畫是觀察這兩個屬性的變化進行的,就會造成動畫的異常,因爲在估算行高機制下,contentSize 的值是一點點地變化更新的,所有 cell 顯示完後纔是最終的 contentSize 值。因爲不會緩存正確的行高,tableView reloadData的時候,會重新計算 contentSize,就有可能會引起 contentOffset 的變化。此外,也看到有開發者被此變化影響到 MJRefresh 上拉刷新功能。
iOS11 下不想使用 Self-Sizing 的話,可以通過以下方式關閉:

self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
if#available(iOS11.0, *) {
self.contentInsetAdjustmentBehavior= .never
self.estimatedRowHeight=0
self.estimatedSectionHeaderHeight=0
self.estimatedSectionFooterHeight=0
}else{
}

iOS11下,如果沒有設置estimateRowHeight的值,也沒有設置rowHeight的值,那contentSize計算初始值是 44 * cell的個數,如下圖:
圖片

2. separatorInset 擴展

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

typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {  
    UITableViewSeparatorInsetFromCellEdges,   //默認值,表示separatorInset是從cell的邊緣的偏移量
    UITableViewSeparatorInsetFromAutomaticInsets  //表示separatorInset屬性值是從一個insets的偏移量
}

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

3. Table Views 和 Safe Area

有以下幾點需要注意:

  • separatorInset 被自動地關聯到 safe area insets,因此,默認情況下,表視圖的整個內容避免了其根視圖控制器的安全區域的插入。
  • UITableviewCell 和 UITableViewHeaderFooterView的 content view 在安全區域內;因此你應該始終在 content view 中使用add-subviews操作。
  • 所有的 headers 和 footers 都應該使用UITableViewHeaderFooterView,包括 table headers 和 footers、section headers 和 footers。

4. 滑動操作(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高度夠大時,會同時顯示image和title。我寫demo測試的時候,因爲每個cell的高度都較小,所以只顯示image,然後我增加cell的高度後,就可以同時顯示image和title了。見下圖對比:
圖片

八. UIBarItem

WWDC通過iOS新增的文件管理App:Files開始介紹,在Files這個APP中能夠看到iOS11中UIKit’s Bars的一些新特性:在瀏覽功能上的大標題視圖(向上滑動後標題會回到原來的UI效果)、橫屏狀態下tab上的文字和icon會變爲左右排列。我用iOS11的模擬器體驗了一下Files這個APP,如下圖所示:
豎屏

橫屏

在iPhone上,tab上的圖標較小,tab bar較小,這樣垂直空間可多放置內容。如果有人看不清楚tab bar上的圖標或文字,可以通過長按tab bar上的任意item,會將該item顯示在HUD上,這樣可以清楚的看清icon和text。對tool bar 和 navigation bar同理,長按item也會放大顯示。如下圖顯示:
這裏寫圖片描述

UIBarItem是UI tab bar item和UI bar button item的父類,要想實現上面介紹的效果,只需要爲UIBarItem 設置landscapeImagePhone屬性,在storyboard中也支持這個設置,對於HUD的image需要設置另一個iOS11新增的屬性:largeContentSizeImage,關於這部分更詳細的討論,可以參考 WWDC2017 Session 215:What’s New in Accessibility

九. iOS11訪問相冊權限變更問題

在更新 iOS11 之後,保存到相冊出現 crash 現象,大家都知道訪問相冊需要申請用戶權限。

相冊權限需要在 info.plist—Property List 文件中添加 NSPhotoLibraryUsageDescription 鍵值對,描述文字不能爲空。

iOS11 之前:訪問相冊和存儲照片到相冊(讀寫權限),需要用戶授權,需要添加NSPhotoLibraryUsageDescription(info.plist 顯示爲 Privacy - Photo Library Usage Description)。

iOS11 之後:默認開啓訪問相冊權限(讀權限),無需用戶授權,無需添加NSPhotoLibraryUsageDescription,適配 iOS11 之前的還是需要加的。 添加圖片到相冊(寫權限),需要用戶授權,需要添加 NSPhotoLibraryAddUsageDescription(info.plist 顯示爲 Privacy - Photo Library Additions Usage Description),否則可能會崩,可能會崩,可能會崩

十. 全新的 HEIC 格式原圖

對於IM的發送原圖功能,iOS11 啓動全新的 HEIC 格式的圖片,iPhone7 以上設備 + iOS11 排出的 live 照片是”.heic”格式圖片,同一張 live 格式的圖片,iOS10 發送就沒問題(轉成了jpg),iOS11就不行
微信的處理方式是一比一轉化成 jpg 格式
QQ和釘釘的處理方式是直接壓縮,即使是原圖也壓縮爲非原圖
也可採取微信的方案,使用以下代碼轉成 jpg 格式

// 0.83能保證壓縮前後圖片大小是一致的
// 造成不一致的原因是圖片的bitmap一個是8位的,一個是16位的
imageData = UIImageJPEGRepresentation([UIImage imageWithData:imageData], 0.83);

十一. iPhoneX

1. TouchID -> FaceID

iPhone X 只有 faceID,沒有touchID,如果你的應用有使用到 touchID 解鎖的地方,這裏要根據設備機型進行相應的適配。

2. LaunchImage

關於iPhoneX(我就不吐槽劉海了…),如果你的APP在iPhoneX上運行發現沒有充滿屏幕,上下有黑色區域,那麼你應該也像我一樣LaunchImage沒有用storyboard而是用的Assets,解決辦法添加1125x2436尺寸的啓動圖。

從bundle中取當前的啓動圖片,圖片名字可以直接到.app文件的bundle中查找。

NSDictionary * dict = @{@"320x480" : @"LaunchImage-700", @"320x568" : @"LaunchImage-700-568h", @"375x667" : @"LaunchImage-800-667h", @"414x736" : @"LaunchImage-800-Portrait-736h", @"375x812" : @"LaunchImage-1100-Portrait-2436h", @"414x896" : @"LaunchImage-1200-Portrait-2688h"};
    NSString * key = [NSString stringWithFormat:@"%dx%d", (int)[UIScreen mainScreen].bounds.size.width, (int)[UIScreen mainScreen].bounds.size.height];
    if ([key isEqualToString:@"414x896"] && IS_SCREEN_61_INCH) {
        _launchImageView.imageView.image = [UIImage imageNamed:@"LaunchImage-1200-Portrait-1792h"];
    } else {
        _launchImageView.imageView.image = [UIImage imageNamed:dict[key]];
    }

3. 狀態欄 和 導航欄

圖片

圖片

關於狀態欄另外兩個需要注意的地方:

  • 不要在 iPhone X 下隱藏狀態欄,一個原因是顯示內容足夠高了,另一個是這樣內容會被劉海切割。
  • 現在通話或者其它狀態下,狀態欄高度不會變化了,程序不需要去做兼容。

4. UITabBar

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

注意橫屏下的 iPhoneX 的底部危險區域高度爲21,UITabBar高度爲32,整個底部佔掉了屏幕的53高度。

5. 一些宏和常量

#define kDevice_Is_iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)
let LL_iPhoneX = (kScreenW == Double(375.0) && kScreenH == Double(812.0) ?true:false)
let kNavibarH = LL_iPhoneX ? Double(88.0) : Double(64.0)
let kTabbarH = LL_iPhoneX ? Double(49.0+34.0) : Double(49.0)
let kStatusbarH = LL_iPhoneX ? Double(44.0) : Double(20.0)

6. 設計原則

在設計方面,蘋果官方文檔 Human Interface Guidelines 有明確要求,下面結合圖例進行說明:

1. 展示出來的設計佈局要求填滿整個屏幕

圖片

2. 填滿的同時要注意控件不要被大圓角和傳感器部分所遮擋

圖片

3. 安全區域以外的部分不允許有任何與用戶交互的控件

圖片

上面這張圖內含信息略多

  • 安全區域以外的部分不允許進行用戶交互的,意味着下面這些情況 Apple 官方是不允許的
    • 狀態欄在非安全區域,文檔中也提到,除非可以通過隱藏狀態欄給用戶帶來額外的價值,否則最好把狀態欄還給用戶
    • 底部虛擬區是替代了傳統home鍵,高度爲34pt,通過上滑可呼起多任務管理,考慮到手勢衝突,這部分也是不允許有任何可交互的控件,但是設計的背景圖要覆蓋到非安全區域
    • 不要讓 界面中的元素 干擾底部的主屏幕指示器
4. 安全區域以外的部分不允許有任何與用戶交互的控件

在橫屏狀態下,不能因爲劉海的原因將內容向左或者向右便宜,要保證內容的中心對稱
圖片

圖片

5. 重複使用現有圖片時,注意長寬比差異。iPhone X 與常規 iPhone 的屏幕長寬比不同,因此,全屏的 4.7 寸屏圖像在 iPhone X 上會出現裁切或適配寬度顯示。所以,這部分的視圖需要根據設備做出適配。

圖片

7. 橫屏適配

關於 safe area,使用 safeAreaLayoutGuide 和 safeAreaInset 就能解決大部分問題,但是橫屏下還可能會產生一些問題,需要額外適配
問題一. 橫屏模式下狀態欄問題
看了下 iPhoneX 模擬器中,桌面沒有橫屏模式,但是所有預裝 App 橫屏都沒有狀態欄。自己新建了工程,發現代碼中重寫 - (BOOL)prefersStatusBarHidden 也無法讓橫屏下出現狀態欄。但是看到網上 這篇文章(戳我可看) ,用比較 hacker 的方法實現的,重寫 setNeedsStatusBarAppearanceUpdate 不做任何事情,導致豎屏切橫屏沒有把狀態條去掉,高度應該還是橫屏下的44,但是這個沒有隱藏的狀態條並沒有影響橫屏模式下從最頂部開始的 SafeArea,所以會導致適配變得有點麻煩,不能完全按照 SafeArea 那一套做相對佈局了(需要考慮這種特殊 case)。

問題二. TableViewCell 的 contentView 的 frame 問題

圖片
產生這個原因代碼是:[headerView.contentView setBackgroundColor:[UIColor headerFooterColor]]

這個寫法看起來沒錯,但是只有在 iPhone X 上有問題,之前所有版本的 iPhone 上 tableView 的 cell 和它的 contentView 的大小是相同的,開發者相對 cell 佈局和相對 contentView 佈局效果上不會有太大區別,但是在 iPhone X 下,由於劉海和圓角的存在,tableView 的 contentView 會被裁切,所以所有的佈局都應該被調整爲相對 contentView 佈局,否則會越界。
圖片
解決方法:設置backgroundView顏色 [headerView.backgroundView setBackgroundColor:[UIColor headerFooterColor]]

8. 滑動手勢

iPhone X 最大的改變就是底部那個無時無刻不存在的 homeBar了,代替了原來home按鍵的功能,系統級的任務切換和回到桌面 、、,都是上滑這個細細的長條。
圖片
所以蘋果爸爸的意思是:

趕緊把你自己寫的上滑手勢乖乖刪掉~

當然如果app確實需要這個手勢,可以打開程序開關覆蓋系統的手勢,但是這樣用戶就需要滑動兩次來回到桌面了,這會讓他們非常懷念home鍵。

9. 鍵盤區別

首先是 iPhone X 下的鍵盤和其他系統有區別,會多出來那個很有趣的animateEmoji工具欄,所以在做鍵盤相關處理的時候要關注兼容性問題,至少:高度不要寫死了……

十二. Xcode 手動編譯失敗

1. 編譯出現一堆奇葩的問題

嘗試將 Applications 文件夾下的 Xcode.app 重命名爲 Xcode9.app 解決了我遇到的問題。

2. Failed to read file attributes for Images.xcassets in Xcode 9

升級到 Xcode9 以後總是遇到這個奇怪的問題,上網查了下,在 stack overflow 這個帖子裏面找到了答案。

Removing the reference of Images.xcassets and adding it again in Project resolved the error.

3. 第三方依賴庫問題

ReactiveCocoa Unknown warning group ‘-Wreceiver-is-weak’,ignored警告
圖片

簡書項目開啓Treat warning as error,所有警告都會被當成錯誤,因此必須解決掉。
RACObserve宏定義如下:

#define RACObserve(TARGET, KEYPATH) \
    ({ \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
        __weak id target_ = (TARGET); \
        [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
        _Pragma("clang diagnostic pop") \
    })

在之前的Xcode中如果消息接受者是一個weak對象,clang編譯器會報receiver-is-weak警告,所以加了這段push&pop,最新的clang已經把這個警告給移除,所以沒必要加push&pop了。
ReactiveCocoa已經不再維護OC版本,大多數OC開發者用的都是2.5這個版本,只能自己fork一份了,誰知github上的v2.5代碼不包含對應的.podspec文件,只好到CocoaPods/Specs上將對應的json文件翻譯成.podspec文件,如果你也有這個需要,可以修改Podfile如下

pod 'ReactiveCocoa', :git => 'https://github.com/zhao0/ReactiveCocoa.git', :tag => '2.5.2'

4. 注意事項

  • Xcode9 打包版本只能是 8.2 及以下版本, 或者 9.0 及更高版本
  • Xcode9 不支持 8.3 和 8.4 版本
  • Xcode9 新打包要在構建版本的時候加入 1024*1024 AppStore Icon
  • 拖動文件或文件夾到工程中,可能會出現代碼文件或圖片沒有加入到 target 中,出現編譯不過或者運行時圖片總是沒顯示,需要特別注意
  • Command 鍵復原。可在 Preferences –> Navigation –> Command-click 中選擇 Jumps to Defintion 即可。

5. 一些好玩的新功能

  • 雞肋的無線調試功能(iPhone的電池…)可在 Window –> Devices and Simulators中勾選那兩個選項。前提是此設備已 run 過並處於同一局域網下。
  • 在 Asset 中,可以創建顏色了。右鍵選擇 New image set,填充RGBA值或十六進制值即可。使用中直接使用新的colorwithname,參數填入創建時的名字即可。不過記得區分系統版本。

6. 模擬器新功能


  • 第一時間很多公司都買不到原價的 iPhoneX 的測試機,會給測試帶來不方便,可以藉助模擬器安裝 app 去做測試工作

啓動運行模擬器:
xcrun instruments -w ‘iPhone 6 Plus’

在已經啓動好的模擬器中安裝應用:
xcrun simctl install booted Calculator.app (這裏要特別注意,是app,不是ipa 安裝時需要提供的是APP的文件路徑)

  • 在全屏模式下使用 Xcode 模擬器

  • 一次打開多個模擬器
  • 縮放模擬器就像調整視窗大小一樣簡單
  • 記錄模擬器的視頻

    在Xcode 9官方的”What’s new”文檔中,蘋果聲稱現在可以錄製模擬器屏幕視頻,即使在舊版本中,只要使用simctl也可以做到,在界面上找不到地方可以啓用視頻錄製(除了iOS 11中的內置屏幕錄製)。
    要獲取視頻檔案,請執行以下代碼:

    xcrun simctl io booted recordVideo –type=mp4

    booted– 表示simctl選擇當前啓動的模擬器,如果你有多個已啓動的模擬器,simctl將選擇當前正在操作的那一個模擬器。

  • 使用 Finder 共享文件到模擬器

    現在,模擬器有了 Finder 擴展功能,你可以直接從 Finder 窗口共享文件。

    你也可以執行以下simctl命令,使用圖像/視頻文件進行類似操作:

    xcrun simctl addmedia booted

    很高興有這樣的操作方法,但是對我而言,將文件拖放至模擬器窗口似乎快很多。

  • 模擬器上打開 URL

    這個也能使用simctl,所以你也可以在舊版本的模擬器上打開自定義的URL schemes。
    拖拽
    以你指定的任何URL執行以下命令:

    xcrun simctl openurl booted

    關於Apple所有URL schemes的列表,請查看文檔.

  • 快速找到應用程序的文件夾

    再來介紹一個simctl的命令,你可以使用單個命令在文件系統上獲取應用程序的資料夾,只需要知道應用程序的bundle identifier並執行以下命令:

    xcrun simctl get_app_container booted

    或者你可以使用open命令在 Finder 中更快打開目標文件夾:

    open xcrun simctl get_app_container booted -a Finder

  • 使用命令行參數(Command Line Args)在模擬器中啓動應用程序

    使用simctl,你也可以從終端機上啓動應用程序,並在其中傳遞一些命令列參數(甚至可以設置一些環境變量)。如果你想在應用程序中插入一些除錯行爲,這將非常有用。
    執行下列命令可以讓你完成這項任務:

    xcrun simctl launch –console booted

    你可以從CommandLine.arguments獲取這些命令行參數(這裏是文件的鏈接)。

  • 透過Bundle ID獲取完整的應用程序消息
    有時找出應用程序的檔案或暫存數據位於文件系統上的位置很有用,如果你需要比simctl get_app_container更全面的資訊,simctl還有一個很好用的小工具,名爲appinfo,它會以下列格式顯示相關資訊:
    執行下面的命令並觀察輸出結果:

    xcrun simctl appinfo booted
  • 十三. xcodebuild 打包命令修改

    升級到最新的 Xcode9 以後,發現 jenkins 自動化打包失敗了,後來看了下來,發現是 xcodebuild 命令簽名失敗,沒有生成 ipa 包。在 這個帖子 中找到了解決方法。

    打包腳本錯誤提示如下:

    Error Domain=IDEDistributionSigningAssetStepErrorDomain Code=0 “Locating signing assets failed.” UserInfo={NSLocalizedDescription=Locating signing assets failed., IDEDistributionSigningAssetStepUnderlyingErrors=(
    “Error Domain=IDEProvisioningErrorDomain Code=9 \”\”HLCG.app\” requires a provisioning profile with the Associated Domains and Push Notifications features.\” UserInfo={NSLocalizedDescription=\”HLCG.app\” requires a provisioning profile with the Associated Domains and Push Notifications features., NSLocalizedRecoverySuggestion=Add a profile to the \”provisioningProfiles\” dictionary in your Export Options property list.}”
    )}
    error: exportArchive: “HLCG.app” requires a provisioning profile with the Associated Domains and Push Notifications features.

    解決辦法:
    編輯 exportOptionsPlist 文件, 在其中添加

    <key>provisioningProfiles</key>
    <dict> <key>com.hula.xxxxxx</key>
    <string>HulaVenueDev</string> (此處名字獲得見下文)
    </dict>

    如:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    <key>provisioningProfiles</key>
    <dict>
    <key>com.hula.xxxxxx</key>
    <string>HulaVenueDev</string>
    </dict>
    <key>compileBitcode</key>
    <false/>
    <key>teamID</key>
    <string>teamIDteamIDteamID</string>
    <key>method</key>
    <string>development</string>
    <key>uploadSymbols</key>
    <true/>
    </dict>
    </plist>

    provisioningProfile 名可以在 apple deveploer 後臺獲得。也可以在 mobileprovision 文件中獲得。
    如圖:
    這裏寫圖片描述

    或 less dev.mobileprovision 找到Name
    這裏寫圖片描述

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章