1.組件介紹
Page是企鵝FM研發(fā)的分頁組件卓舵,包括支持分頁非交互切換(通過方法調(diào)用導(dǎo)航切換)和交互切換(屏幕的手勢滑動(dòng)),多個(gè)分頁Controller和View的管理囱修。
1.1需求背景
為什么棄用UIPageViewController,首先介紹一下UIPageViewController,這是系統(tǒng)為開發(fā)者定制的分頁組件外傅,提供了兩種分頁切換的效果羊瘩,一是滑動(dòng) 二是翻頁。且提供了前后切換的回調(diào)柒莉。
a) UIPageViewController在iOS8以下的系統(tǒng)運(yùn)行是有問題的闻坚,可以參考stackFlow上的癥狀描述https://stackoverflow.com/questions/12939280/uipageviewcontroller-navigates-to-wrong-page-with-scroll-transition-style/12939384#12939384
This is actually a bug in UIPageViewController. It occurs only with
the scroll style (.Scroll) and only after calling
setViewControllers:direction:animated:completion: with animated:YES.
Thus there are two workarounds:
Don’t use UIPageViewControllerTransitionStyleScroll.
Or, if you call setViewControllers:direction:animated:completion:, use animated:NO.
To see the bug clearly, call
setViewControllers:direction:animated:completion: and then, in the
interface (as user), navigate left (back) to the preceding page
manually. You will navigate back to the wrong page: not the preceding
page at all, but the page you were on when
setViewControllers:direction:animated:completion: was called.
The reason for the bug appears to be that, when using the scroll
style, UIPageViewController does some sort of internal caching. Thus,
after the call to setViewControllers:direction:animated:completion:, it
fails to clear its internal cache. It thinks it knows what the preceding
page is. Thus, when the user navigates leftward to the preceding page,
UIPageViewController fails to call the dataSource method
pageViewController:viewControllerBeforeViewController:, or calls it with
the wrong current view controller.
大意是說使用.Scroll的時(shí)候,UIPageViewController做了內(nèi)部緩存的排序兢孝,當(dāng)調(diào)用
setViewControllers:direction:animated:completion:
時(shí) 它認(rèn)為自己知道了前一個(gè)的分頁存在窿凤,當(dāng)調(diào)用前一個(gè)頁面的時(shí)候,就不會(huì)去調(diào)用dataSource的方法跨蟹。
b)
UIPageViewController的DataSource和Delegate的接口過于簡單雳殊,對于比較復(fù)雜的情況(比如除了分頁以外還有其他View的情況下)無法處理。參照下面的例圖窗轩,我有一個(gè)tab下面有小黃條夯秃,跟著手勢橫向滑動(dòng)的同時(shí)也橫向滑動(dòng),這里系統(tǒng)的UIPageViewController無法支持品姓。其外寝并,我還需要子頁面縱向滑動(dòng)時(shí)候去修改Cover和Tab的frame。所以UIPageViewController無法滿足比較復(fù)雜的需求腹备。
c) 低配的機(jī)器會(huì)產(chǎn)生卡頓問題衬潦,因?yàn)橄到y(tǒng)的UIPageViewController,在快速切換的時(shí)候,會(huì)釋放掉不用的頁面植酥,所以在快速回切的時(shí)候會(huì)造成卡頓镀岛,可以參考下面的性能測試。
綜上所述友驮,棄用了系統(tǒng)的UIPageViewController漂羊。
1.2使用說明
使用非常簡單,繼承組件的類卸留,實(shí)現(xiàn)相應(yīng)的delegate和datasourc就可以了走越。
Page的例圖如下:
頁面層次關(guān)系如下:
圖中由一個(gè)圖片,3個(gè)欄目 (詳情耻瑟,節(jié)目旨指,評論)和一個(gè)List組成赏酥。可以分為三個(gè)層次谆构,Cover,Tab和Page裸扶。
Page組件層次關(guān)系如下,
圖中的ShowListController是節(jié)目分頁搬素,AlbumListController是專輯分頁.
2.組件架構(gòu)設(shè)計(jì)
2.1 架構(gòu)介紹
類圖如下:
簡要說明下各個(gè)協(xié)議的作用:
FMPageDataSource,? 提供子頁面呵晨,子頁面的個(gè)數(shù),子頁面展示的frame給PageController熬尺。
FMPageDelegate,? 提供頁面交互切換和非交互切換的回調(diào)給上層以及頁面的縱向滑動(dòng)和橫向滑動(dòng)的contentoffset給上層摸屠。
FMTabDataSource,? 提供TabView的具體展示效果。
FMTabDelegate,? 提供TabView的點(diǎn)擊響應(yīng)給上層猪杭。
FMCoverController,? 提供CoverView給CoverController.
其中餐塘,F(xiàn)MTabController默認(rèn)遵循FMTabDataSource,FMTabDelegateSource,FMPageDataSource,FMPageDelegate協(xié)議。FMCoverController遵循FMCoverDatasource協(xié)議皂吮。
2.2 接口設(shè)計(jì)
接口遵循高內(nèi)聚和低耦合的特性戒傻,只把Delegate和DataSource開放給上層,同時(shí)做接口分離蜂筹,把Page,Tab,Cover特性的分離需纳。 代碼如下:
@interfaceFMTabController : FMBusinessViewController @interfaceFMCoverController : FMTabController
2.3 Child頁面的生命周期管理和切換。
1.UIScrollView支持分頁效果艺挪,手勢處理及交互操作多個(gè)回調(diào)方法可以實(shí)現(xiàn)頁面的切換效果不翩。
2.生命周期管理有兩種方式 a.頻繁地add/remove ChildController b.使用下面的代碼實(shí)現(xiàn)生命周期的管理:
1)shouldAutomaticallyForwardAppearanceMethods2)beginAppearanceTransition: animated:3)endAppearanceTransition
a.會(huì)產(chǎn)生一個(gè)重大缺陷,就是頻繁切換的卡頓問題麻裳。
b.不需要頻繁地去調(diào)用add/remove,1)方法避免了 add/remove產(chǎn)生的生命周期口蝠,2)和3)保證了開發(fā)者可以自己控制ChildController的生命周期。
Page的生命周期圖如下:
初次或者reloadPage
交互切換和非交互切換
2.4 性能問題擴(kuò)展
以下通過Iphone5 模擬器 10.3系統(tǒng)津坑,與UIPageViewController做了性能上的對比妙蔗。
UIPageViewController 快速切換內(nèi)存占用情況
UIPageViewController 快速切換GPU占用情況
Page組件快速切換內(nèi)存占用情況
Page組件快速切換GPU占用情況
從上圖中內(nèi)存占用圖標(biāo)的波動(dòng)情況可以看出UIPageViewController在快速切換的時(shí),會(huì)盡可能快地釋放掉不用的controller及其view(主要是view)以保證內(nèi)存占用較小疆瑰,所以圖標(biāo)指標(biāo)先才會(huì)頻繁的波動(dòng),與UIPageViewController作對比眉反,Page組件用空間換時(shí)間的策略避免頁面卡頓。
3.技術(shù)實(shí)現(xiàn)的難點(diǎn)
從技術(shù)上看穆役,可以分為以下四個(gè)點(diǎn):
3.1 接口的設(shè)計(jì)寸五。
接口的設(shè)計(jì),是整個(gè)架構(gòu)的核心耿币,如果開始設(shè)計(jì)不好梳杏,會(huì)導(dǎo)致后續(xù)的擴(kuò)展就是加屬性和加方法,導(dǎo)致代碼越來越龐大,以致無法維護(hù)秘狞,所以盡量保證簡潔叭莫,職能單一,可擴(kuò)展烁试。
起初為了讓delegate和datasource可以從Controller分離出去,把delegate和datasource都暴露了出去拢肆,但這樣相當(dāng)于多了5個(gè)屬性减响,對于上層來說并不便于理解這些接口,仿照UITableViewController郭怪,由繼承的方式實(shí)現(xiàn)這些協(xié)議支示,讓接口更加簡潔。
3.2 頁面縱向滑動(dòng)跟隨Tab和Cover一起滑動(dòng)鄙才。
通過上面的動(dòng)態(tài)圖颂鸿,可以知道,Page組件有這樣一個(gè)功能,子頁面縱向滑動(dòng)會(huì)跟隨Tab和Cover一起向上滑動(dòng)攒庵,其中cover的滑動(dòng)的實(shí)現(xiàn)是監(jiān)聽ChildController的ScrollView的contentOffset嘴纺,修改Tab的height或y。Scrollview的滑動(dòng)有一個(gè)難點(diǎn)浓冒,怎樣保證ScrollView的向下滑動(dòng)的反彈處緊貼Tab栽渴,而Scrollview又可以向上滑動(dòng)到導(dǎo)航欄。
首先Scrollview的可見范圍是整屏的稳懒,也就是設(shè)置frame為整屏闲擦,Scrollview滑動(dòng)的范圍,就由ContentInset,ContentOffset
共同決定场梆。因?yàn)槲覀冎繳IScrollView的滑動(dòng)范圍會(huì)緊貼scrollView的bounds墅冷。所以首先,修改ContentInset的Top為-tabH-tabY或油,可以保證向下滑動(dòng)到Tab的下邊緣處反彈寞忿,又由于frame是整屏的,向上滑動(dòng)時(shí)候就可以滑動(dòng)導(dǎo)航欄装哆,代碼如下:
scrollView.contentInset =? UIEdgeInsetsMake([self.dataSource pageTop], contentInset.left, contentInset.bottom, contentInset.right);scrollView.frame = CGRectMake(0,0,Screen_Width,Screen_Height)
其中的pageTop就是tab的下邊緣處罐脊。
3.3 不相鄰頁面切換的問題
不相鄰頁面的非交互切換會(huì)閃過中間的頁面,產(chǎn)生不好的用戶體驗(yàn)蜕琴,本組件的解決方法是
非交互切換萍桌,模擬切換的動(dòng)畫,這里需要考慮的一個(gè)復(fù)雜情況是第一次動(dòng)畫還未結(jié)束就開始第二次凌简,這時(shí)候需要提前結(jié)束第一次動(dòng)畫上炎。修改后的效果圖如下,
3.4 平衡性能的問題。
因?yàn)镻age要管理多個(gè)controller和view藕施,如果子頁面到1000寇损,甚至10000個(gè)怎樣去處理。比如微信閱讀的一本書就可能有10000頁裳食。所以這里如果全部都保存就可能產(chǎn)生一個(gè)問題矛市,內(nèi)存會(huì)不會(huì)過大。
觀察UIPageViewController,它到一定的內(nèi)存限制诲祸,會(huì)主動(dòng)去釋放很久沒翻過的頁面浊吏。所以這里,可以使用LRUCache的機(jī)制救氯,只保存一定數(shù)量的頁面找田。由于本應(yīng)用并不涉及到過多的子頁面,考慮的時(shí)間花銷和內(nèi)存着憨,全部保存了所有頁面墩衙。