内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

高仿网易新闻频道选择器

2018-12-03 21:14 出处:清屏网 人气: 评论(0

前段时间公司做一个新闻类的项目,需要支持频道编辑,缓存等功能,界面效果逻辑就按照最新版的网易新闻来,网上没找到类似的轮子,二话不说直接开撸,为了做到和网易效果一模一样还是遇到不少坑和细节,这在此分享出来,自己做个记录,大家觉得有用的话也可以参考。支持手动集成或者cocoapods集成。

项目地址

github.com/yd2008/YDCh…

最终效果

其实基本就和网易一毛一样了啦,只是为了更加直观还是贴出两张图片

调起方式

因为要弹出一个占据全屏的控件,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!)
}
复制代码

分享给小伙伴们:
本文标签: 网易新闻选择器

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号