高仿网易新闻频道选择器
前言
前段時間公司做一個新聞類的項目,需要支持頻道編輯,緩存等功能,界面效果邏輯就按照最新版的網易新聞來,網上沒找到類似的輪子,二話不說直接開擼,為了做到和網易效果一模一樣還是遇到不少坑和細節,這在此分享出來,自己做個記錄,大家覺得有用的話也可以參考。支持手動集成或者cocoapods集成。
項目地址
github.com/yd2008/YDCh…
最終效果
其實基本就和網易一毛一樣了啦,只是為了更加直觀還是貼出兩張圖片
調起方式
因為要彈出一個占據全屏的控件,7.0之前可能是加在window上,但是后面蘋果不建議這么做,所以還是直接present一個控制器出來是最優的選擇。
public class YDChannelSelector: UIViewController 復制代碼創建
非常簡單,遵守數據源協議和代理協議
class ViewController: UIViewController, YDChannelSelectorDataSource, YDChannelSelectorDelegate// 頻道選擇控制器private lazy var channelSelector: YDChannelSelector = {let sv = YDChannelSelector()sv.dataSource = selfsv.delegate = self// 是否支持本地緩存用戶功能 默認開啟// sv.isCacheLastest = falsereturn sv}() 復制代碼基于接口傻瓜的原則,呼出窗口最簡單的方法就是系統自帶的present方法就ok。
present(channelSelector, animated: true, completion: nil) 復制代碼傳遞數據
作為一個頻道選擇器,它需要知道哪些關鍵信息呢?
- 頻道名字
- 頻道是否是固定欄目
- 頻道自己的原始數據
基于以上需求,我設計了頻道結構體
public struct SelectorItem {/// 頻道名稱public var channelTitle: String!/// 是否是固定欄目public var isFixation: Bool!/// 頻道對應初始字典或模型public var rawData: Any?public init(channelTitle: String, isFixation: Bool = false, rawData: Any?) {self.channelTitle = channelTitleself.isFixation = isFixationself.rawData = rawData} } 復制代碼數據源代理方法和tableView一致,上手簡單容易
public protocol YDChannelSelectorDataSource: class {func numberOfSections(in selector: YDChannelSelector) -> Intfunc selector(_ selector: YDChannelSelector, numberOfItemsInSection section: Int) -> Intfunc selector(_ selector: YDChannelSelector, itemAt indexPath: IndexPath) -> SelectorItem } 復制代碼代理
用戶做了各種操作后如何通知控制器當前狀態
public protocol YDChannelSelectorDelegate: class {/// 數據源發生變化func selector(_ selector: YDChannelSelector, didChangeDS newDataSource: [[SelectorItem]])/// 點擊了關閉按鈕func selector(_ selector: YDChannelSelector, dismiss newDataSource: [[SelectorItem]])/// 點擊了某個頻道func selector(_ selector: YDChannelSelector, didSelectChannel channelItem: SelectorItem) } 復制代碼核心思路
如果你只是打算直接用的話那下面已經不用看了,因為以下是記錄初版功能實現的核心思路以及難點介紹,如果感興趣想自己擴展功能或者自定義的話可以看看。
寫在前面: ios9以后蘋果又添加了很多強大的api,所以本插件主要基于幾個新api實現,整個邏輯還是很清晰明了。主要是很多細節比較惡心,后期調試了很久。
控件選擇一眼就能看出 UICollectionView
private lazy var collectionView: UICollectionView = {let layout = UICollectionViewFlowLayout()layout.minimumLineSpacing = itemMarginlayout.minimumInteritemSpacing = itemMarginlayout.itemSize = CGSize(width: itemW, height: itemH)let cv = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)cv.contentInset = UIEdgeInsets.init(top: 0, left: itemMargin, bottom: 0, right: itemMargin)cv.backgroundColor = UIColor.whitecv.showsVerticalScrollIndicator = falsecv.delegate = selfcv.dataSource = selfcv.register(YDChannelSelectorCell.self, forCellWithReuseIdentifier: YDChannelSelectorCellID)cv.register(YDChannelSelectorHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: YDChannelSelectorHeaderID)cv.addGestureRecognizer(longPressGes)return cv }() 復制代碼最近刪除 & 用戶操作緩存
基于網易的邏輯,在操作時會出現一個新的section叫最近刪除,dismiss時把最近刪除的頻道下移到我的欄目,思路就是在viewWillApperar時操縱數據源,添加最近刪除section,在viewDidDisappear時整理用戶操作,移除最近刪除section,與此同時進行用戶操作的緩存和讀取,具體實現代碼如下:
public override func viewWillAppear(_ animated: Bool) {super.viewWillAppear(animated)// 根據需求處理數據源if isCacheLastest && UserDefaults.standard.value(forKey: operatedDS) != nil { // 需要緩存之前數據 且用戶操作有存儲// 緩存原始數據源if isCacheLastest { cacheDataSource(dataSource: dataSource!, isOrigin: true) }var bool = falselet newTitlesArrs = dataSource!.map { $0.map { $0.channelTitle! } }let orginTitlesArrs = UserDefaults.standard.value(forKey: originDS) as? [[String]]// 之前有存過原始數據源if orginTitlesArrs != nil { bool = newTitlesArrs == orginTitlesArrs! }if bool { // 和之前數據相等 -> 返回緩存數據源let cacheTitleArrs = UserDefaults.standard.value(forKey: operatedDS) as? [[String]]let flatArr = dataSource!.flatMap { $0 }var cachedDataSource = cacheTitleArrs!.map { $0.map { SelectorItem(channelTitle: $0, rawData: nil) }}for (i,items) in cachedDataSource.enumerated() {for (j,item) in items.enumerated() {for originItem in flatArr {if originItem.channelTitle == item.channelTitle {cachedDataSource[i][j] = originItem}}}}dataSource = cachedDataSource} else { // 和之前數據不等 -> 返回新數據源(不處理)}}// 預處理數據源var dataSource_t = dataSourcedataSource_t?.insert(latelyDeleteChannels, at: 1)dataSource = dataSource_tcollectionView.reloadData()}public override func viewDidDisappear(_ animated: Bool) {super.viewDidDisappear(animated)// 移除界面后的一些操作dataSource![2] = dataSource![1] + dataSource![2]dataSource?.remove(at: 1)latelyDeleteChannels.removeAll()} 復制代碼用戶操作相關
移動主要依賴9.0新增的InteractiveMovement系列接口,通過給collectionView添加長按手勢并監聽拖動的location實現item拖動效果:
private func handleLongGesture(ges: UILongPressGestureRecognizer) {guard isEdit == true else { return }switch(ges.state) {case .began:guard let selectedIndexPath = collectionView.indexPathForItem(at: ges.location(in: collectionView)) else { break }collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)case .changed:collectionView.updateInteractiveMovementTargetPosition(ges.location(in: ges.view!))case .ended:collectionView.endInteractiveMovement()default:collectionView.cancelInteractiveMovement()} } 復制代碼這里有個小坑就是cell自己的長按手勢會和collectionView的長按手勢沖突,需要在創建cell的時候做沖突解決:
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {......// 手勢沖突解決longPressGes.require(toFail: cell.longPressGes)...... } 復制代碼仔細觀察發現網易的有個細節,就是點擊item的時候要先閃爍一下在進入編輯狀態,但是觸碰事件會被collectionView攔截,所以要先自定義collectionView,重寫func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?做下轉換和提前處理:
fileprivate class HitTestView: UIView {open var collectionView: UICollectionView!/// 攔截系統觸碰事件public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {if let indexPath = collectionView.indexPathForItem(at: convert(point, to: collectionView)) { // 在某個cell上let cell = collectionView.cellForItem(at: indexPath) as! YDChannelSelectorCellcell.touchAnimate()}return super.hitTest(point, with: event)} }復制代碼在編輯模式頻道不能拖到更多欄目里面,需要還原編輯動作,蘋果提供了現成接口,我們只需要實現相應邏輯即可:
/// 這個方法里面控制需要移動和最后移動到的IndexPath(開始移動時) /// - Returns: 當前期望移動到的位置 public func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {let item = dataSource![proposedIndexPath.section][proposedIndexPath.row]if proposedIndexPath.section > 0 || item.isFixation { // 不是我的欄目 或者是固定欄目return originalIndexPath} else {return proposedIndexPath} } 復制代碼用戶操作后的數據源處理
用戶操作完后對數據源要操作方法是func handleDataSource(sourceIndexPath: IndexPath, destinationIndexPath: IndexPath), 調用時間有兩個,一是拖動編輯后調用,二就是點擊事件調用,為了數據源越界統一在此處理:
private func handleDataSource(sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {let sourceStr = dataSource![sourceIndexPath.section][sourceIndexPath.row]if sourceIndexPath.section == 0 && destinationIndexPath.section == 1 { // 我的欄目 -> 最近刪除latelyDeleteChannels.append(sourceStr)}if sourceIndexPath.section == 1 && destinationIndexPath.section == 0 && !latelyDeleteChannels.isEmpty { // 最近刪除 -> 我的欄目latelyDeleteChannels.remove(at: sourceIndexPath.row)}dataSource![sourceIndexPath.section].remove(at: sourceIndexPath.row)dataSource![destinationIndexPath.section].insert(sourceStr, at: destinationIndexPath.row)// 通知代理delegate?.selector(self, didChangeDS: dataSource!)// 存儲用戶操作cacheDataSource(dataSource: dataSource!) } 復制代碼以上就是項目核心思路和具體實現過程,歡迎使用,求Star~ 后續還會添加oc版本和外部的tab滑動條,敬請期待!你有好的建議或者問題也可隨時pull request或者issue我。
轉載于:https://juejin.im/post/5bfb91dee51d4548657d0265
總結
以上是生活随笔為你收集整理的高仿网易新闻频道选择器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第十一周编程总结--助教
- 下一篇: error总结