内容简介:前段时间公司做一个新闻类的项目,需要支持频道编辑,缓存等功能,界面效果逻辑就按照最新版的网易新闻来,网上没找到类似的轮子,二话不说直接开撸,为了做到和网易效果一模一样还是遇到不少坑和细节,这在此分享出来,自己做个记录,大家觉得有用的话也可以参考。支持手动集成或者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!)
}
复制代码
以上所述就是小编给大家介绍的《高仿网易新闻频道选择器》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Security Testing Cookbook
Paco Hope、Ben Walther / O'Reilly Media / 2008-10-24 / USD 39.99
Among the tests you perform on web applications, security testing is perhaps the most important, yet it's often the most neglected. The recipes in the Web Security Testing Cookbook demonstrate how dev......一起来看看 《Web Security Testing Cookbook》 这本书的介绍吧!