内容简介:前段时间公司做一个新闻类的项目,需要支持频道编辑,缓存等功能,界面效果逻辑就按照最新版的网易新闻来,网上没找到类似的轮子,二话不说直接开撸,为了做到和网易效果一模一样还是遇到不少坑和细节,这在此分享出来,自己做个记录,大家觉得有用的话也可以参考。支持手动集成或者cocoapods集成。其实基本就和网易一毛一样了啦,只是为了更加直观还是贴出两张图片
前段时间公司做一个新闻类的项目,需要支持频道编辑,缓存等功能,界面效果逻辑就按照最新版的网易新闻来,网上没找到类似的轮子,二话不说直接开撸,为了做到和网易效果一模一样还是遇到不少坑和细节,这在此分享出来,自己做个记录,大家觉得有用的话也可以参考。支持手动集成或者cocoapods集成。
项目地址
最终效果
其实基本就和网易一毛一样了啦,只是为了更加直观还是贴出两张图片
调起方式
因为要弹出一个占据全屏的控件,7.0之前可能是加在window上,但是后面苹果不建议这么做,所以还是直接present一个控制器出来是最优的选择。
public class YDChannelSelector: UIViewController 复制代码
创建
非常简单,遵守数据源协议和代理协议
class ViewController: UIViewController, YDChannelSelectorDataSource, YDChannelSelectorDelegate // 数据源 因为至少有当前栏目和可添加栏目,所以是二维数组 var selectorDataSource: [[SelectorItem]]? { didSet { // 网络异步获取成功时赋值即可 channelSelector.dataSource = selectorDataSource } } // 频道选择控制器 private lazy var channelSelector: YDChannelSelector = { let sv = YDChannelSelector() sv.delegate = self // 是否支持本地缓存用户功能 // sv.isCacheLastest = false return 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 = channelTitle self.isFixation = isFixation self.rawData = rawData } } 复制代码
实现数据源代理里的数据接口
public protocol YDChannelSelectorDataSource: class { /// selector 数据源 var selectorDataSource: [[SelectorItem]]? { get } } 复制代码
代理
用户做了各种操作后如何通知控制器当前状态
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 = itemMargin layout.minimumInteritemSpacing = itemMargin layout.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.white cv.showsVerticalScrollIndicator = false cv.delegate = self cv.dataSource = self cv.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 = false let 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 = dataSource dataSource_t?.insert(latelyDeleteChannels, at: 1) dataSource = dataSource_t collectionView.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拖动效果:
@objc 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! YDChannelSelectorCell cell.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!) } 复制代码
以上所述就是小编给大家介绍的《高仿网易新闻频道选择器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。