一. iPad的一些常識(shí)
-
iPad的屏幕尺寸和分辨率
- 建議沒有做過iPad適配的同學(xué), 在蘋果官方文檔查看一下不同型號(hào)iPad的尺寸
- 注意一下點(diǎn)與像素的區(qū)別, 尤其是Retina屏幕
-
iPhone和iPad開發(fā)之間的一些區(qū)別
- iPad的屏幕相比iPhone較大, 因此能容納更多內(nèi)容, 并且多數(shù)iPad的UI排布都是左右分屏
- iPad的鍵盤多了一個(gè)退出鍵盤的按鈕, 因此不需要手動(dòng)寫
endEditing
這樣的代碼了 - API:
- 基本所有的API, 在iPad和iPhone上都是共用的, 只是有些效果會(huì)有些不同
- iPad比iPhone多了一些特有類: 如
UIPopoverController
和UISplitViewController
, 這兩個(gè)類分別對應(yīng)下拉菜單和分屏
- 屏幕方向
- iPhone只有三個(gè)方向: 即豎屏, 左橫屏和右橫屏(一般App只需要豎屏)
- iPad則支持四個(gè)方向: 豎屏, 返轉(zhuǎn)豎屏, 左橫屏和有橫屏(蘋果官方建議, 最好同時(shí)支持橫豎屏兩個(gè)方向)
二. 項(xiàng)目分析
iPad的QQ空間, 使用的是分屏模式, 即左側(cè)
dock
邊欄, 右側(cè)content
內(nèi)容展示-
本項(xiàng)目比較麻煩的點(diǎn)就在于, 在轉(zhuǎn)換屏幕的時(shí)候, 如何設(shè)置好
dock
和content
的約束- 尺寸大小發(fā)生改變
- 部分控件的排列結(jié)構(gòu)也會(huì)發(fā)生改變: 如橫向排列改為縱向排列
- 按鈕的狀態(tài)變化: 橫豎屏不同的狀態(tài), 按鈕展示的內(nèi)容也不同(只展示圖片/圖文展示)
-
方案選擇(布局方式):
- Autoresizing
- 該方案通常用來解決父控件和子控件之間相對關(guān)系的問題
- 因此Autoresizing只能改變父子控件的相對位置/尺寸, 但是當(dāng)橫豎屏發(fā)生變化時(shí), 無法重新排列控件
- 從Autoresizing的各種屬性也能得知, 他是更改相對位置和尺寸
UIViewAutoresizingFlexibleLeftMargin等等
- Autolayout
- Autolayout是從iOS6.0之后推出的布局方案, 用于解決相對控件的位置/尺寸問題
- 他可以很便捷的布局控件之間的約束關(guān)系, 并且可以讓控件之間產(chǎn)生耦合, 互相耦合的控件可以一同發(fā)生改變
- 但他和Autoresing有相同的缺點(diǎn), 就是不能改變控件的排列方式(橫向排列改為縱向排列)
- 并且, 控件之間的耦合性過強(qiáng), 一旦控件發(fā)生改變, 和他有關(guān)聯(lián)的控件都要變
- UIStackView
- iOS9.0后退出的新控件, 他可以快速的將一些控件進(jìn)行水平/垂直排布, 并且可以設(shè)置間距
- 控件之間的耦合性很弱, 耦合性只針對于控件和StackView之間產(chǎn)生
- 一般用于實(shí)現(xiàn)一些簡單的水平/垂直排列
- iOS9.0+才能使用, 對于目前普遍從iOS8.0開始適配來說, 該方案不可選
- Sizeclass
- iOS8.0推出的功能, 可以解決不同屏幕狀態(tài)下的排列方式和重設(shè)控件的內(nèi)容樣式
- Sizeclass最大的優(yōu)點(diǎn)就在于他可以區(qū)分不同的屏幕尺寸來設(shè)置約束, 但是詳細(xì)設(shè)置還要配合其他方案
- 無法區(qū)分iPad的橫豎屏!!!
- 純代碼
- 較為繁瑣, 很麻煩, 鄙人比較不喜歡純代碼的方式
- 優(yōu)勢: 優(yōu)秀的代碼風(fēng)格, 可以讓你的控件耦合性很低, 并且復(fù)用性很高
- 對于iPad這種橫豎屏發(fā)生變化后, 控件的位置/大小/內(nèi)容都會(huì)發(fā)生改變的情況下, 最優(yōu)選擇就是純代碼搭建
- Autoresizing
三. 登錄界面搭建
此項(xiàng)目的重點(diǎn)在于如何做好iPad情況下的橫豎屏適配, 所以業(yè)務(wù)邏輯并沒有實(shí)現(xiàn)
-
登錄界面的重點(diǎn):
- 對于橫豎屏而言, 最好不要將約束參照設(shè)置為屏幕的四邊, 當(dāng)屏幕旋轉(zhuǎn)時(shí), 屏幕尺寸會(huì)產(chǎn)生很大的變化, 導(dǎo)致控件拉伸會(huì)很嚴(yán)重
- 在開發(fā)的過程中, 一定要記得, 盡量將可復(fù)用的功能抽取為工具類!!也就是封裝思想!! + 在封裝工具類時(shí), 涉及的block的回調(diào)
-
具體實(shí)現(xiàn)部分
-
登錄的工具類的封裝(block的回調(diào))
@implementation QQLoginTool + (void)loginByAccount:(NSString *)account password:(NSString *)password result:(void (^)(BOOL))loginResult { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if ([account isEqualToString:@"qq"] && [password isEqualToString:@"123"]) { loginResult(YES); } else { loginResult(NO); } }); } @end
-
登錄的執(zhí)行
- (IBAction)loginBtnClick:(id)sender { // 0. 開始加載動(dòng)畫 [self.loginLoading startAnimating]; // 1. 判斷是否內(nèi)容完全 _loginBtn.enabled = (_accountTF.text.length && _pwdTF.text.length); // 2. 判斷內(nèi)容密碼是否正確 [QQLoginTool loginByAccount:_accountTF.text password:_pwdTF.text result:^(BOOL isSuccess) { if (isSuccess) { // 登錄成功跳轉(zhuǎn)界面 QQHomeViewController *homeVC = [[QQHomeViewController alloc] init]; [UIApplication sharedApplication].keyWindow.rootViewController = homeVC; } else { // 登錄失敗, 執(zhí)行彈跳動(dòng)畫 CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"]; animation.values = @[@-50, @0, @50, @0]; animation.duration = 0.2; animation.repeatCount = 3; [_animationView.layer addAnimation:animation forKey:@"error"]; // 提示賬號(hào)密碼錯(cuò)誤 [QQShowMessageTool showErrorMessage:@"賬號(hào)或密碼錯(cuò)誤!"]; // 3. 停止加載動(dòng)畫 [self.loginLoading stopAnimating]; } }]; }
-
三. 主頁的實(shí)現(xiàn)
-
重點(diǎn)
-
dockView
和contentView
在切換橫豎屏?xí)r的適配 -
對于一整塊具有多個(gè)子界面的View, 最好的辦法是將其劃分成不同的區(qū)域, 并且抽出不同的類
- QQDockTopView
- QQDockMiddleView
- QQDockBottomView
- 對于一些固定的取值(橫豎屏狀態(tài)下的dock和contentView的尺寸), 單獨(dú)放在一個(gè)類或PCH文件中, 方便管理
-
-
dockView
和contentView
的布局- 在切換橫豎屏之后, 屏幕的尺寸就會(huì)發(fā)生改變, 而這時(shí)候我們要獲取屏幕準(zhǔn)確的尺寸, 來重新布局控件
- -viewWillLayoutSubviews
- -viewDidLayoutSubviews
- 經(jīng)過檢測, 以上兩個(gè)方法, 是在屏幕轉(zhuǎn)動(dòng)之后, 獲取屏幕尺寸最準(zhǔn)確的方法, 所以更新控件frame的方法應(yīng)該放在這里
- 鄙人在處理控件不同狀態(tài)下的尺寸時(shí), 使用的是單例模型, 蘋果公司建議大家盡量少用PCH文件的
創(chuàng)建一個(gè)QQHomeViewFrameItem模型類, 并且制作為單例
在這個(gè)類中, 判斷當(dāng)前屏幕的橫豎屏狀態(tài)
-
根據(jù)橫豎屏狀態(tài)獲取各個(gè)控件的尺寸/位置值
#import "QQHomeViewFrameItem.h" static QQHomeViewFrameItem *_frameItem; @implementation QQHomeViewFrameItem #pragma mark - 確定dockView的寬度 + (instancetype)shareFrameItem { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _frameItem = [[QQHomeViewFrameItem alloc] init]; }); return _frameItem; } - (BOOL)isLandScape { CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; return (screenWidth > screenHeight); } - (CGFloat)dockWidth { return (self.isLandScape ? 210 : 70); } // 等等控件的尺寸位置..... @end
- 在切換橫豎屏之后, 屏幕的尺寸就會(huì)發(fā)生改變, 而這時(shí)候我們要獲取屏幕準(zhǔn)確的尺寸, 來重新布局控件
-
dockView
的一些重點(diǎn)-
dockView
在切換橫豎屏之后, 內(nèi)部的控件也會(huì)相應(yīng)的發(fā)生變化, 所以要在layoutSubviews
方法中, 重新布局子控件 -
dockMiddleView
中的按鈕, 在橫屏的時(shí)候顯示圖片+文字, 而在豎屏的時(shí)候只顯示圖片, 因此要自定義按鈕- (CGRect)titleRectForContentRect:(CGRect)contentRect
- (CGRect)imageRectForContentRect:(CGRect)contentRect
-
給上面兩個(gè)方法增加一個(gè)判斷, 根據(jù)橫豎屏的狀態(tài)來設(shè)置按鈕的圖片/文字的布局
// 個(gè)人認(rèn)為對于設(shè)置按鈕內(nèi)容布局來說, 非常實(shí)用的方法 - (CGRect)titleRectForContentRect:(CGRect)contentRect { if (self.frameItem.isLandScape) { return CGRectMake(contentRect.size.width * radio, 0, contentRect.size.width * (1 - radio), contentRect.size.height); } else { return CGRectZero; } } - (CGRect)imageRectForContentRect:(CGRect)contentRect { if (self.frameItem.isLandScape) { return CGRectMake(0, 0, contentRect.size.width * radio, contentRect.size.height); } else { return contentRect; } }
- 按鈕監(jiān)聽的傳遞
在日常開發(fā)中, MVC設(shè)計(jì)模式, 一定要終于誰的事情交給誰處理, 而這里的麻煩點(diǎn)就在于按鈕是被View包裝起來的, 并且View還有一個(gè)dockView來包裝, 但是按鈕點(diǎn)擊的方法實(shí)現(xiàn)應(yīng)該交由控制器來管理
因此這里要做多層傳遞, 最簡單的辦法是使用通知來跨層傳遞
-
但鄙人這里使用的是代理傳遞: 將一個(gè)控件的代理, 賦值給另一個(gè)控件的代理, 然后交由控制器管理
#pragma mark - QQDockBottomView - (void)addBtn { NSArray *imageNames = @[@"tabbar_blog", @"tabbar_mood", @"tabbar_photo"]; for (int i = 0; i < imageNames.count; i++) { UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; [btn setImage:[UIImage imageNamed:imageNames[i]] forState:UIControlStateNormal]; btn.tag = i; [btn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:btn]; } } - (void)btnClicked:(UIButton *)btn { if ([self.delegate respondsToSelector:@selector(dockBottomViewClickButtonWithType:)]) { [self.delegate dockBottomViewClickButtonWithType:btn.tag]; } } #pragma mark - QQDockView // 這里要重寫Setter, 將代理傳給外界 - (void)setDelegate:(id<QQdockViewDelegate>)delegate { _delegate = delegate; // 將代理傳給Dock的代理 self.middleView.delegate = _delegate; self.bottomView.delegate = _delegate; } #pragma mark - QQDockViewController // 傳遞給控制器, 由控制器來實(shí)現(xiàn)代理方法, 監(jiān)聽按鈕的點(diǎn)擊 - (void)dockBottomViewClickButtonWithType:(DockBottomViewButtonType)type { switch (type) { case DockBottomViewButtonTypeRizhi: NSLog(@"日志"); break; case DockBottomViewButtonTypeShuoshuo: NSLog(@"說說"); break; case DockBottomViewButtonTypeCamera: NSLog(@"相機(jī)"); break; } }
-
小結(jié):
- iPad開發(fā)麻煩的地方就在于橫豎屏的適配問題
- 在這個(gè)案例中使用了一次代理傳遞, 比較不好理解
- 對于比較復(fù)雜并且多變的控件, 筆者建議大家使用純代碼的方式來搭建, 比較靈活, 也便于維護(hù)
- iOS開發(fā)多注重封裝思想, 能抽調(diào)出來的功能一定要封裝為一個(gè)工具類
- 但是, 不要為了解耦去過度解耦, 弄得自己都混亂了