抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

这是我参与更文挑战的第25天,活动详情查看: 更文挑战

前两天的构建回顾

在之前的文章中,我们分别构建了HomeViewModel以及BaseViewController和BaseTableViewController的构建,由于有这个两个基础组件的编写,我们首页基本上就剩下交互与数据绑定了。

不过在此之前,我们的首页有一个轮播图,我们先把这个给处理了。

代码编写

这次轮播图我使用了第三方库——FSPagerView。如果有人使用过FSCalendar,应该应该就有印象了,没错就是那位写日历大佬写的轮播组件。

昨天我也说了,与其费时费力的通过RxSwift去封装第三库,不如直接调用来的简单容易,也可以少掉一些头发。

再加上我完全对这个库不熟悉,所以这里就直接当个Api调用工程师吧。

另外由于之前已经抽取了基类和ViewModel,代码量不大,我就直接上全部的代码,大家仔细看代码注释就好:

import Foundation

import RxSwift
import RxCocoa
import NSObject_Rx
import SnapKit
import MJRefresh
import Kingfisher
import FSPagerView

class HomeController: BaseTableViewController {
/// 接受轮播图的数据源
var itmes: [Banner] = []

override func viewDidLoad() {
    super.viewDidLoad()
    setupUI()
}

}

extension HomeController {
private func setupUI() {

    title = "首页"
    
    /// 获取indexPath
    tableView.rx.itemSelected
        .bind { [weak self] (indexPath) in
            self?.tableView.deselectRow(at: indexPath, animated: false)
            print(indexPath)
        }
        .disposed(by: rx.disposeBag)
    
    
    /// 获取cell中的模型
    tableView.rx.modelSelected(Info.self)
        .subscribe(onNext: { [weak self] model in
            print("去下一个页面")
        })
        .disposed(by: rx.disposeBag)
    
    /// 创建HomeViewModel
    let viewModel = HomeViewModel(disposeBag: rx.disposeBag)
    
    /// 下拉刷新
    tableView.mj_header?.rx.refresh
        .asDriver()
        .drive(onNext: {
            viewModel.loadData(actionType: .refresh)
            
        })
        .disposed(by: rx.disposeBag)
    
    /// 上拉加载
    tableView.mj_footer?.rx.refresh
        .asDriver()
        .drive(onNext: {
            viewModel.loadData(actionType: .loadMore)
            
        })
        .disposed(by: rx.disposeBag)

    /// 绑定数据
    viewModel.dataSource
        .asDriver(onErrorJustReturn: [])
        .drive(tableView.rx.items) { (tableView, row, info) in
            if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? InfoViewCell {
                cell.info = info
                return cell
            }else {
                let cell = InfoViewCell(style: .subtitle, reuseIdentifier: "Cell")
                cell.info = info
                return cell
            }
        }
        .disposed(by: rx.disposeBag)
    
    /// 空数据绑定
    viewModel.dataSource.map { $0.count == 0 }.bind(to: isEmpty).disposed(by: rx.disposeBag)
    
    /// 下拉与上拉状态绑定到tableView
    viewModel.refreshSubject
        .bind(to: tableView.rx.refreshAction)
        .disposed(by: rx.disposeBag)
    
    //MARK:- 轮播图的设置,这一段基本上就典型的Cocoa代码了
    
    /// 初始化pagerView 
    let pagerView = FSPagerView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 16.0 * 9))
    
    /// 设置轮播图的代理与数据
    pagerView.dataSource = self
    pagerView.delegate = self
    
    /// 注册轮播图cell
    pagerView.register(FSPagerViewCell.self, forCellWithReuseIdentifier: "FSPagerViewCell")
    
    /// 轮播时间
    pagerView.automaticSlidingInterval = 3.0
    
    /// 无限轮播
    pagerView.isInfinite = true
    
    /// 将pagerView赋值给tableView.tableHeaderView
    tableView.tableHeaderView = pagerView
    
    /// 轮播图的小圆点配置
    
    /// 初始化FSPageControl
    let pageControl = FSPageControl(frame: CGRect.zero)
    
    /// 小圆点的个数
    pageControl.numberOfPages = itmes.count
    
    /// 当前小圆点所在位置
    pageControl.currentPage = 0
    
    /// 数据为1时隐藏pageControl组件
    pageControl.hidesForSinglePage = true
    
    /// 将pageControl添加到pagerView
    pagerView.addSubview(pageControl)
    
    /// pageControl移到pagerView的最上方
    pagerView.bringSubviewToFront(pageControl)
    
    /// 使用SnapKit进行布局,左、右、底与pagerView靠紧,高度为40
    pageControl.snp.makeConstraints { make in
        make.leading.trailing.bottom.equalTo(pagerView)
        make.height.equalTo(40)
    }
    
    /// viewModel的数据驱动,将models赋值给items,重新设置pageControl的小圆点个数,reloadData
    viewModel.banners.asDriver(onErrorJustReturn: []).drive { [weak self] models in
        self?.itmes = models
        pageControl.numberOfPages = models.count
        pagerView.reloadData()
    }.disposed(by: rx.disposeBag)
}

}

/// FSPagerViewDataSource数据源,基本上和TableView的数据源大同小异
extension HomeController: FSPagerViewDataSource {
func numberOfItems(in pagerView: FSPagerView) -> Int {
return itmes.count
}

func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
    let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "FSPagerViewCell", at: index)
    if let imagePath = itmes[index].imagePath, let url = URL(string: imagePath) {
        cell.imageView?.kf.setImage(with: url)
    }
    return cell
}

}

/// FSPagerViewDataSource代理,基本上和TableView的代理大同小异
extension HomeController: FSPagerViewDelegate {
func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) {
pagerView.deselectItem(at: index, animated: false)
let item = itmes[index]
print(“点击了轮播图的(item)”)
}

/// 这里通过willDisplay方法去改变当前的小圆点
func pagerView(_ pagerView: FSPagerView, willDisplay cell: FSPagerViewCell, forItemAt index: Int) {
    guard let pageControl = pagerView.subviews.last as? FSPageControl else {
        return
    }
    pageControl.currentPage = index
}

}
复制代码

说明

好了上面的代码基本上我都逐行写了注释,应该比较清晰了,挑一些之前没讲过的说说:

  • 我尝试使用了序列Driver:
tableView.mj_header?.rx.refresh
            .asDriver()
            .drive(onNext: {
                viewModel.loadData(actionType: .refresh)
                
            })
            .disposed(by: rx.disposeBag)
复制代码

其实没有什么特别的,就是一种在主线程进行订阅的序列,非常适合去驱动UI。

  • 我尝试当数据为空的时候与BaseTableViewController的isEmpty进行绑定:
viewModel.dataSource.map { $0.count == 0 }.bind(to: isEmpty).disposed(by: rx.disposeBag)
复制代码

就是当数据为0时,会走基类的DZNEmptyDataSet中的相关方法,经过验证尝试更改map { $0.count == 0 }中的逻辑,是可行的。

  • 由于这次的列表的Cell稍微特殊点,有文字有图,文字有可能会比较长导致单个Cell的高度不确定于是使用了自定义Cell——InfoViewCell

如何自定义一个Cell,使得其能自适应高度,撑满Cell,以及Cell中的图片根据模型中的字段显示和不显示,这个篇幅会有点长,需要单独说明。

  • 最后说一说轮播图的Banner模型和单个Cell的Info模型,在点击它们后,都是可以跳转到一个WebView页面,加载一个URL的:

而关键是这两个模型长的不太一样:

Banner模型

struct Banner : Codable {
    
    var id : Int?
    
    var title : String?
    
    var originId: Int? = nil
    
    var link: String? { url }
        
    let desc : String?
    
    let imagePath : String?
    let isVisible : Int?
    let order : Int?
    
    let type : Int?
    let url : String?
}

复制代码

Info模型:

/// 单个信息模型,用于首页,项目,公众号,搜索关键词,体系,收藏夹
struct Info : Codable {
    
    var title : String?
    
    var link: String?
    /// 我的收藏接口originId才是文章的标识符,id没有用,不要使用
    var originId: Int?
    /// 不是我的收藏接口,拿到的id就是文章的标识符,需要通过这个字段进行收藏与取消收藏的操作,此时originId为nil
    var id : Int?
    
    let apkLink : String?
    let audit : Int?
    let author : String?
    let canEdit : Bool?
    let chapterId : Int?
    let chapterName : String?
    let collect : Bool?
    let courseId : Int?
    let desc : String?
    let descMd : String?
    let envelopePic : String?
    let fresh : Bool?

let niceDate : String?
let niceShareDate : String?
let origin : String?

let prefix : String?
let projectLink : String?
let publishTime : Int?
let selfVisible : Int?
let shareDate : Int?
let shareUser : String?
let superChapterId : Int?
let superChapterName : String?
let tags : [Tag]?

let type : Int?
let userId : Int?
let visible : Int?
let zan : Int?

}

struct Tag : Codable {

let name : String?
let url : String?

}
复制代码

大家可以看到,banner模型中并没有link字段,而有url字段,于是我自己写了一个只读计算属性var link: String? { url }

另外要说明的是这个Info这个模型在很多地方通用,同时InfoViewCell也在很多列表通用,但是有两个字段有有些区别:

id字段: 不是我的收藏接口,拿到的id就是文章的标识符,需要通过这个字段进行收藏与取消收藏的操作,此时originId为nil。

originId字段:我的收藏接口originId才是文章的标识符,id有值,但是这个id不是文章的标识符,不能使用。

一般要将两个不同的模型糅合成一个模型的方法就是创建一个基类,然后让Info和Banner分别继承这个基类,传值的时候传基类模型即可,基类的写法大概就是这:

class SomeBase : Codable {
    
    var title : String?
    
    var link: String?

var originId: Int?

var id : Int?

}

class Info: SomeBase {
….
}

class Bananer: SomeBase {
….
}
复制代码

注意我这样写的时候,模型都换成了class,因为struct是无法继承的。

但是既然我写的是Swift编程,Swift编程是鼓励使用面向协议的编程方式的。

我们通过写一个协议,并使得Banner与Info的分类去遵守协议即可。


import Foundation

protocol WebLoadInfo {
var id: Int? { set get }
var originId: Int? { set get }
var title: String? { set get }
var link: String? { get }
}

extension Info: WebLoadInfo {}

extension Bananer: WebLoadInfo {}
复制代码

注意:大家注意看Info与Banner两个模型的id、originId、title、link我都是用var修饰,其实模型用工具类生成的时候都是用let修饰的,只是为了遵守WebLoadInfo协议,改成了var。

虽然有改动,但是侵略性小,问题迎刃而解。

这样我们在传值的时候,使用WebLoadInfo就可以了。

总结

本篇文章主要讲解了HomeController的编写:

  • 由于之前有BaseViewController与BaseTableViewController做基类,并且HomeViewModel已经将逻辑写好。重点是FSPagerView的使用,详细的用法我已经在上面的代码注释写过了。

  • 另外就是对于Banner模型与Info模型如何将其公共部分糅合成一个公共类便于调用,我通过继承与协议两种思路进行了讲解,继承是典型的面向对象思路,而面向协议则是Swift比较推荐,每个人有每个人的习惯,这里不做强求。

明日继续

本篇其实有一个内容没有讲完,那就是InfoViewCell的布局,考虑自己的撸代码和写作的能力,这部分需要另起炉灶讲解了。

大家加油。