參考文章:
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中顯示大標題,如果想要在控制不同頁面大標題的顯示,可以通過設置當前頁面的navigationItem
的largeTitleDisplayMode
屬性;
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,至於是多少?
我用宏定義的方式設置的,你也可以自定義,或者使用其他的方式確定其大小。
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都定義了margin
。margin
指的是控件顯示內容部分的邊緣和控件邊緣的距離。
可以用layoutMargins
或者layoutMarginsGuide
屬性獲得view的margin
, margin
是視圖內部的一部分。layoutMargins
允許獲取或者設置UIEdgeInsets
結構的margin
。layoutMarginsGuide
則獲取到只讀的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));
layoutMargins
是UIEdgeInsets
結構體類型的屬性:
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
中引入的topLayoutGuide
和 bottomLayoutGuide
在 iOS 11 中被廢棄了!取而代之的就是safeArea
的概念,safeArea
是描述你的視圖部分不被任何內容遮擋的方法。 它提供兩種方式:safeAreaInsets
或safeAreaLayoutGuide
來提供給你safeArea
的參照值,即 insets
或者layout guide
。 safeArea
區域如圖所示:
如果有一個自定義的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獲取這些命令行參數(這裏是文件的鏈接)。
有時找出應用程序的檔案或暫存數據位於文件系統上的位置很有用,如果你需要比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