Swift-低仿搜狐新聞標籤頁效果

宮城良田發表於2017-07-03

前言:先看下效果

Tips:

  1. 這是用Swfit寫的一個小Demo,用UICollectionView實現的拖拽排序,點選排序的效果。
  2. 我所用的UICollectionView的排序方法是系統預設的方法,優點是比較簡單,不用自己去計算太多。缺點是隻支援iOS 9.0以後的版本。
  3. 此Demo僅供參考,還有很多地方不完善,抽空我會再修改完善的,也歡迎各位給我提出缺點,並指正!

?用法簡單介紹

  • ViewController就是一個首頁的普通控制器,當點選+的時候,就會push頻道管理(也就是標籤列表)頁面。
  • ViewController裡自定義了兩個陣列,我的頻道(myChannels)和更多頻道(moreChannels)
  • 在點選+跳轉到頻道管理頁面的點選方法裡面有一個回撥方法,即:將選中的頻道、以及自定義後的頻道回傳到此頁面。
var myChannels = ["推薦", "熱點", "北京", "視訊",
                  "社會", "娛樂", "問答", "汽車",
                  "財經", "軍事", "體育", "段子",
                  "美女", "時尚", "國際", "趣圖",
                  "健康", "特賣", "房產", "養生",
                  "歷史", "育兒", "小說", "教育",
                  "搞笑"]
var moreChannels = ["科技", "直播", "數碼", "美食",
                    "電影", "手機", "旅遊", "股票",
                    "科學", "動漫", "故事", "收藏",
                    "精選", "語錄", "星座", "美圖",
                    "政務", "闢謠", "火山直播", "中國新唱將",
                    "彩票", "快樂男生", "正能量"]

override func viewDidLoad() {
    super.viewDidLoad()

    navigationItem.title = "王紅慶"
    view.backgroundColor = UIColor.white

    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(popToChannelListViewController))
}

func popToChannelListViewController() -> () {

    let channelVC = HQChannelListViewController(myChannel: myChannels, moreChannel: moreChannels)
    channelVC.selectCallBack = { (myChannel, moreChannel, selectIndex) -> () in
        self.navigationItem.title = myChannel[selectIndex]
        self.myChannels = myChannel
        self.moreChannels = moreChannel
    }
    navigationController?.pushViewController(channelVC, animated: true)
}複製程式碼

?所有的事情都交給HQChannelListViewController來處理

  • 首先定義一些可能用到的常量
private let SCREEN_WIDTH = UIScreen.main.bounds.size.width
private let SCREEN_HEIGHT = UIScreen.main.bounds.size.height
private let HQChannelListCellIdentifier = "HQChannelListCellIdentifier"
private let HQChannelListHeaderViewIdentifier = "HQChannelListHeaderViewIdentifier"
private let itemW: CGFloat = (SCREEN_WIDTH - 60) / 4複製程式碼
  • 自定義流水佈局,設定佈局的一些屬性
// MARK: - 自定義佈局屬性
class HQChannelListViewLayout: UICollectionViewFlowLayout {

    override func prepare() {
        super.prepare()

        headerReferenceSize = CGSize(width: SCREEN_WIDTH, height: 40)
        itemSize = CGSize(width: itemW, height: itemW * 0.5)
        minimumInteritemSpacing = 5
        minimumLineSpacing = 5
        sectionInset = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
    }
}複製程式碼
  • 自定義CollectionHeaderView
// MARK: - CollectionHeaderView
class HQChannelListHeaderView: UICollectionReusableView {

    var editCallBack: (() -> ())?
    var text: String? {
        didSet {
            label.text = text
        }
    }

    func edit() -> () {

        if editCallBack != nil {
            editCallBack!()
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupUI() {

        addSubview(label)
        addSubview(button)
        backgroundColor = UIColor.groupTableViewBackground
    }

    private lazy var label: UILabel = {

        let label = UILabel(frame: self.bounds)
        label.frame.origin.x = 20
        return label
    }()

    lazy var button: UIButton = {

        let btn = UIButton(type: .custom)
        btn.setTitle("編輯", for: .normal)
        btn.setTitle("完成", for: .selected)
        btn.setTitleColor(UIColor.init(colorLiteralRed: 255 / 255.0, green: 0 / 255.0, blue: 0 / 255.0, alpha: 0.7), for: .normal)
        btn.titleLabel?.font = UIFont.systemFont(ofSize: 13)
        btn.frame = CGRect(x: SCREEN_WIDTH - 65, y: 10, width: 50, height: 25)
        btn.addTarget(self, action: #selector(edit), for: .touchUpInside)

        btn.layer.cornerRadius = 12.5
        btn.layer.borderWidth = 1
        btn.layer.borderColor = UIColor.init(colorLiteralRed: 255 / 255.0, green: 0 / 255.0, blue: 0 / 255.0, alpha: 0.7).cgColor
        return btn
    }()
}複製程式碼
  • 自定義Cell
// MARK: - 自定義Cell
class HQChannelListCell: UICollectionViewCell {

    var edit = true {
        didSet {
            imageView.isHidden = !edit
        }
    }

    var text: String? {
        didSet {
            label.text = text
        }
    }
    var textColor: UIColor = UIColor.darkGray {
        didSet {
            label.textColor = textColor
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = UIColor.white
        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupUI() {

        self.addSubview(label)
        label.addSubview(imageView)
    }

    private lazy var label: UILabel = {

        let label = UILabel(frame: self.bounds)
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 15)
        return label
    }()

    private lazy var imageView: UIImageView = {

        let imageView = UIImageView(frame: CGRect(x: self.bounds.size.width - 12, y: -3, width: 15, height: 15))
        imageView.image = UIImage(named: "close")
        imageView.isHidden = true
        return imageView
    }()
}複製程式碼
  • 定義回撥方法、給Item新增長按手勢,並處理長按的一些狀態(方法均為UICollectionView提供的方法,只支援iOS 9.0以後的版本)
class HQChannelListViewController: UIViewController {

    // 選擇一個頻道後的回撥
    var selectCallBack: ((_ myChannel: [String], _ moreChannel: [String], _ selectIndex: Int) -> ())?
    let headerTitle = [["我的頻道", "更多頻道"], ["拖動頻道排序", "點選新增頻道"]]
    var array1 = ["推薦"]
    var array2 = ["有聲"]
    var isEdit = false

    init(myChannel: [String], moreChannel: [String]) {

        array1 = myChannel
        array2 = moreChannel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "頻道管理"
        view.addSubview(collectionView)
    }

    // MARK: - longPress
    func longPress(tap: UILongPressGestureRecognizer) -> () {

        if !isEdit {
            isEdit = !isEdit
            collectionView.reloadData()
            return
        }
        let point = tap.location(in: tap.view)
        let sourceIndexPath = collectionView.indexPathForItem(at: point)

        switch tap.state {
        case UIGestureRecognizerState.began:
            collectionView.beginInteractiveMovementForItem(at: sourceIndexPath!)

        case UIGestureRecognizerState.changed:
            collectionView.updateInteractiveMovementTargetPosition(point)

        case UIGestureRecognizerState.ended:
            collectionView.endInteractiveMovement()

        case UIGestureRecognizerState.cancelled:
            collectionView.cancelInteractiveMovement()
        default:
            break
        }
    }

    // MARK: - lazy
    private lazy var collectionView: UICollectionView = {

        let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: HQChannelListViewLayout())
        collectionView.backgroundColor = UIColor.groupTableViewBackground
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(HQChannelListCell.classForCoder(), forCellWithReuseIdentifier: HQChannelListCellIdentifier)
        collectionView.register(HQChannelListHeaderView.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: HQChannelListHeaderViewIdentifier)
        let gesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress))
        collectionView.addGestureRecognizer(gesture)
        return collectionView
    }()
}複製程式碼
  • 實現CollectionView的資料來源方法
// MARK: - UICollectionViewDataSource
extension HQChannelListViewController: UICollectionViewDataSource {

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return section == 0 ? array1.count : array2.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HQChannelListCellIdentifier, for: indexPath) as! HQChannelListCell
        cell.text = indexPath.section == 0 ? array1[indexPath.item] : array2[indexPath.item]
        cell.edit = (indexPath.section == 0 && indexPath.item == 0 || indexPath.section == 1) ? false : isEdit
        if !isEdit {
            cell.textColor = (indexPath.section == 0 && indexPath.item == 0) ? UIColor.init(colorLiteralRed: 255 / 255.0, green: 0 / 255.0, blue: 0 / 255.0, alpha: 0.7) : UIColor.darkGray
        } else {
            cell.textColor = (indexPath.section == 0 && indexPath.item == 0) ? UIColor.lightGray : UIColor.darkGray
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: HQChannelListHeaderViewIdentifier, for: indexPath) as! HQChannelListHeaderView
        headerView.text = isEdit ? headerTitle[1][indexPath.section] : headerTitle[0][indexPath.section]
        headerView.button.isSelected = isEdit

        if indexPath.section > 0 {
            headerView.button.isHidden = true
        } else {
            headerView.button.isHidden = false
        }

        headerView.editCallBack = { [weak self] in
            self?.isEdit = !(self?.isEdit)!
            collectionView.reloadData()
        }

        return headerView
    }

    func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
        // 設定第一組的第一個不能被移動
        if indexPath.section == 0 && indexPath.item == 0 {
            return false
        }
        return true
    }
}複製程式碼
  • 實現CollectionView的代理方法,在將選中的Item移動到目標的Item上的時候,我的方法處理的不是太好。但是想不到什麼好法子,歡迎大家給我提思路,提建議。
// MARK: - UICollectionViewDelegate
extension HQChannelListViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        if indexPath.section == 0 {
            if isEdit {
                if indexPath.item == 0 {
                    return
                }
                let obj = array1[indexPath.item]
                array1.remove(at: indexPath.item)
                array2.insert(obj, at: 0)
                collectionView.moveItem(at: indexPath, to: NSIndexPath(item: 0, section: 1) as IndexPath)
            } else {
                if selectCallBack != nil {
                    selectCallBack!(array1, array2, indexPath.item)
                    _ = navigationController?.popViewController(animated: true)
                }
            }
        } else {

            let obj = array2[indexPath.item]
            array2.remove(at: indexPath.item)
            array1.append(obj)
            collectionView.moveItem(at: indexPath, to: NSIndexPath(item: array1.count - 1, section: 0) as IndexPath)
        }
    }

    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

        /*
         1.以下方法是處理移動後的陣列中的元素'刪除'或'新增'問題.
         2.不這樣處理,就會崩潰.自己演算法水平有限,也是真的沒想到什麼比較好的辦法.
         3.可能有人比較較真,提到如果真的像搜狐那麼多'section'如何處理.個人感覺,目前市面上比較火的幾家新聞,只有搜狐分的比較多,其它像'頭條'或者'網易'也就都只有兩組而已.
         4.如果大家有什麼好的方法,歡迎拍磚.我願意像各位前輩學習.
         */
        if sourceIndexPath.section == 0 && destinationIndexPath.section == 0 {
            let obj = array1[sourceIndexPath.item]
            array1.remove(at: sourceIndexPath.item)
            array1.insert(obj, at: destinationIndexPath.item)
        }
        if sourceIndexPath.section == 0 && destinationIndexPath.section == 1 {
            let obj = array1[sourceIndexPath.item]
            array1.remove(at: sourceIndexPath.item)
            array2.insert(obj, at: destinationIndexPath.item)
        }
        if sourceIndexPath.section == 1 && destinationIndexPath.section == 0 {
            let obj = array2[sourceIndexPath.item]
            array2.remove(at: sourceIndexPath.item)
            array1.insert(obj, at: destinationIndexPath.item)
        }
        if sourceIndexPath.section == 1 && destinationIndexPath.section == 1 {
            let obj = array2[sourceIndexPath.item]
            array2.remove(at: sourceIndexPath.item)
            array2.insert(obj, at: destinationIndexPath.item)
        }
    }
}複製程式碼

?總結

Swift造的第一個輪子,主要是給自己增加點積累,也練練Swift的一些用法。
現在還存在的一些不盡人意的地方:

  1. 長按之後是變成編輯狀態,不像《頭條》或者《搜狐》那樣長按之後變成編輯也可以繼續拖動。
  2. 選中Item沒有放大的效果,確實影響使用者體驗。
  3. 如果將Item我的頻道移動到更多頻道裡面,刪除的x(小叉叉)依然存在。
  4. 我的頻道裡面第一個Item本意上我是不希望他可以被移動的,但是如果將其它的Item移動到第一個位置依然可以,背離了我的初衷。
  5. 仔細觀察了一下,《頭條》或者《搜狐》的更多頻道裡,如果將我的頻道中的Item移動到更多頻道裡,《搜狐》只是放在更多頻道裡面的最後一個位置,《頭條》是放在第一個的位置,並沒有放哪裡都行,我突然又感覺我自己的又有點多此一舉了。看來有個好的產品經理還是很重要的。

以上是我個人的一些總結,我相信一定還有我自己沒有注意到的地方存在問題。歡迎各位給我提寶貴意見。我會積極改正的!!!

DEMO傳送門:HQChannelListView

相關文章