50天iOS挑戰(zhàn)(Swift) - 第10天:制作應(yīng)用啟動引導(dǎo)頁面
50天放椰,每天一個(gè)Swift語言的iOS練手項(xiàng)目愉粤,覆蓋iOS開發(fā)的主要知識衣厘。貴在堅(jiān)持,重在思考
文章列表:http://www.reibang.com/nb/13566182
Github項(xiàng)目:https://github.com/Minecodecraft/50DaysOfSwift
簡介
很多應(yīng)用在用戶初次啟動時(shí)影暴,會展示一個(gè)What's new頁面型宙,如果打造一個(gè)多屏幕適配的啟動界面,同時(shí)又保證低耦合性呢妆兑?Let's do it!
本節(jié)將介紹啟動界面的制作芯勘,下一節(jié)介紹登錄界面的多屏幕適配。
由于代碼較為復(fù)雜衡怀,文章中只講制作過程安疗,建議結(jié)合代碼同步理解。
主要知識點(diǎn): Xib設(shè)計(jì)模式蝶桶、控制器解耦掉冶、代碼重構(gòu)、數(shù)據(jù)模型恢共、KVC璧亚、KVO、CollectionView透硝、PageControl疯搅、控件約束(屏幕適配)
豎屏模式:
橫屏模式:
設(shè)計(jì)思路
1幔欧、設(shè)計(jì)架構(gòu)
由于引導(dǎo)界面只是龐大項(xiàng)目的一個(gè)小模塊,所以要盡可能減少耦合性觉义。
本著各司其職的思路浴井,我們將引導(dǎo)界面和登錄界面分別放到兩個(gè)故事版中。
引導(dǎo)界面需要用到的cell厉碟,采用Xib方式構(gòu)建。(也可代碼構(gòu)建)
應(yīng)用啟動時(shí)崭参,判斷啟動某個(gè)界面的工作款咖,交給NavigationController。對于控制器間通訊海洼,采用代碼塊內(nèi)指針方式富腊,以避免強(qiáng)引用循環(huán)(也可使用代理、閉方式避免)是整。
2民假、界面設(shè)計(jì)
由于要兼容橫豎屏羊异,所以界面約束的設(shè)計(jì)格外重要。
根據(jù)自己需求添加界面約束即可野舶,在此不再贅述。
3赴蝇、配置CollectionView
我們采用Xib構(gòu)造巢掺,所以要在CollectionView注冊Cell
collectionView.register(UINib.init(nibName: "PageViewCell", bundle: nil), forCellWithReuseIdentifier: guideCell)
collectionView.register(UINib.init(nibName: "LoginViewCell", bundle: nil), forCellWithReuseIdentifier: loginCell)
3陆淀、建立數(shù)據(jù)模型
開發(fā)中我們與服務(wù)器通信時(shí)經(jīng)常采用json先嬉,數(shù)據(jù)常以模型的形式存在。為了減少耦合性含懊,我們采用數(shù)據(jù)模型來表示每一個(gè)引導(dǎo)頁的內(nèi)容。
蘋果為我們提供了更簡單的對屬性賦值方式:KVC
var imageName: String?
var title: String?
var detail: String?
init(dict: [String: Any]) {
super.init()
setValuesForKeys(dict)
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {
// Do something
}
記著重寫setValue:forUndefinedKey:酥筝,因?yàn)槟J(rèn)實(shí)現(xiàn)會拋出異常雏门,程序crash
完成模型建立后,我們就可以通過鍵值對的方式對數(shù)據(jù)模型賦值宙帝。
4募闲、最后一頁隱藏按鈕
從GIF中可以看到,翻到最后一頁時(shí)我們要隱藏PageControl和兩個(gè)按鈕控件沪编。
我們采用更改控件約束的方式
if off {
pageControlBottom.constant = -20
skipButtonTop.constant = -50
continueButtonTop.constant = -50
// disable button to avoid crash
skipButton.isEnabled = false
continueButton.isEnabled = false
} else {
pageControlBottom.constant = 10
skipButtonTop.constant = 0
continueButtonTop.constant = 0
skipButton.isEnabled = true
continueButton.isEnabled = true
}
當(dāng)然年扩,這只是改變了其frame厨幻,系統(tǒng)并不一定立即重繪,我們調(diào)用layoutIfNeed方法進(jìn)行重繪况脆。
// layout subviews
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
5格了、復(fù)用頁面跳轉(zhuǎn)方法
由于劃屏和點(diǎn)擊按鈕都需要調(diào)用翻頁方法,將翻頁/頁面跳轉(zhuǎn)/按鈕隱藏方法封裝弹惦,提供接口供其他方法調(diào)用悄但。
優(yōu)化/Bug解決
1、橫屏旋轉(zhuǎn)時(shí)頁面位置錯(cuò)誤
因?yàn)锳uto Layout Guide調(diào)整layout時(shí)助泽,并不會調(diào)整對應(yīng)的index,所以需要在willTransition中手動調(diào)用
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
let indexPath = IndexPath(item: pageControl.currentPage, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
但是這樣還是沒有解決隐解,原因在于collectionView在旋轉(zhuǎn)之前完成了scrollToItem方法诫睬。
解決方法很簡單,提交一個(gè)延時(shí)0.01秒任務(wù)溜嗜。
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
let indexPath = IndexPath(item: pageControl.currentPage, section: 0)
DispatchQueue.main.asyncAfter(deadline: .now()+0.01) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
2架谎、解決導(dǎo)航控制器無rootViewController的問題
我們在APPDelegate中設(shè)置UIWindow為導(dǎo)航控制器谷扣,如下面代碼
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let nvc = MainNavigationController()
window?.rootViewController = nvc
return true
}
但是,當(dāng)我們在導(dǎo)航控制器中初始化引導(dǎo)頁面控制器/登錄界面控制器/登陸成功界面控制器時(shí)裹匙,可能出現(xiàn)沒有view沒有成功present的情況末秃。原因和步驟1很類似,將present任務(wù)延時(shí)提交即可惰匙。
如下面代碼铃将,導(dǎo)航控制器的初始化過程:
class MainNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.cyan
// 添加delay時(shí)間,放置在window初始化之前顯示
if isFirstRun() {
perform(#selector(showGuideController), with: nil, afterDelay: 0.01)
}
else if isLoggedIn() {
// show
}
else {
perform(#selector(showLoginController), with: nil, afterDelay: 0.01)
}
}
func isFirstRun() -> Bool {
return true
}
func isLoggedIn() -> Bool {
return false
}
@objc func showGuideController() {
let storyboard = UIStoryboard(name: "Guide", bundle: nil)
let guideVC = storyboard.instantiateInitialViewController() as! GuideViewController
present(guideVC, animated: false) {
// do something
}
}
func finishShowGuideController() {
dismiss(animated: false, completion: nil)
showLoginController()
}
@objc func showLoginController() {
let storyboard = UIStoryboard(name: "Login", bundle: nil)
let loginVC = storyboard.instantiateInitialViewController() as! LoginViewController
present(loginVC, animated: false) {
// do something
}
}
}
詳細(xì)過程請見代碼,下一節(jié)將介紹登錄界面的屏幕適配問題悯仙,包括輸入時(shí)屏幕上移、重繪輸入框等功能實(shí)現(xiàn)稚虎。
項(xiàng)目源碼
項(xiàng)目源碼已傳至Github:(https://github.com/Minecodecraft/50DaysOfSwift)
后續(xù)教程偎捎、代碼茴她、庫持續(xù)更新,歡迎Star關(guān)注丈牢。希望可以對大家有所幫助