授权转载,作者:ZeroJ
前言:
之前闲着的时候就随便模仿斗鱼的界面写了一些界面, 最初的时候在网上找到的获取直播的sign加密方式还是可用的, 当时还使用IJKMediaFramework, 集成了直播视频的获取和播放, 当时的项目也就还是挺庞大的, 不过大约在7.21 左右斗鱼的api升级了, 然后就不能获取到直播了, 所以现在把项目中的直播相关的全部都删除了。
目前项目中就只能看到部分的界面和一些网络的请求了, 项目是使用swift来实现的, 但是如果你是最初接触swift的话, 有一些地方可能可以参考一下. 项目地址
一些页面的效果如下
关于项目的一些解释
一. 最初是使用MVC来设计的项目的, 最近开始接触MVVM设计模式,在网上找到的各种MVVM的相关的资料, 就把先前的这个项目拿来改动试试, 然后在改的时候发现, 很多时候不可能做到理想的MVVM架构的, 因为可能使用到第三方的东西导致不能很方便的使用MVVM, 另外就是, 个人觉得简单的界面使用MVVM就是在浪费时间
这里关于MVVM就简单的提一下了
MVVM = model, view(viewController), viewModel
在MVVM中, 每个view(viewController)理论上对应一个viewModel, view(viewController)负责界面的布局, 和响应用户的点击, 以及展示页面...
viewModel用于处理view的所有的展示逻辑(请求网络, 操作数据库, 格式化字符串...), 而且完美的viewModel里面是不应该引入UIKit的, 所以viewModel就拥有view所需要的所有的数据, viewModel中只进行数据的加工, 能够对这些数据进行必要的操作, 然后让对应的view更新数据.
因为view是拥有viewModel的, 所以要实现view和viewModel的通信(view更新的时候同步更新viewModel中的数据)很简单, 但是要实现viewModel和view的绑定就很难得, 有时候你可以选择(kvo, 代理, 通知, block...), 但是很多时候实现都是非常的麻烦的, 因为你需要做到在viewModel中更新的时候同步更新对应的view的状态.
所以这个时候你就需要一个响应式编程的框架,来实现view和viewModel的(单)双向绑定, 比如OC中你可以用ReactiveCocoa, 在swift中, 你可以使用ReactiveCocoa, RxSwift, Bond...(推荐RxSwift, 号称是符合RX官方的设计, 跨平台的设计理念, RxJava, RxJS...可以类似的使用)
另外有人提出更符合MVVM的是viewModel只暴露一些输入和输出信号给view, 通过将这些信号绑定到view上面实现和view的同步更新, 而viewModel不暴露方法给view, 比如按钮的点击和viewModel的一个按钮点击的信号绑定, 在viewModel中通过订阅这个信号处理按钮的点击, 而不是在view中调用viewModel的响应按钮点击的方法... 不过个人更倾向于暴露方法, 因为感觉使用信号的话对第三方的框架依赖太大了
model和MVC中的model基本相似的角色, 这里就不介绍了, 关于MVVM的更多的介绍, 推荐看这一系列的博客
二. 项目最初是集成了IJKMediaFramework并且实现了直播的一些功能, 不过由于斗鱼Api的变动, 就全部给移除了
三. 项目使用纯swift写的, 所以很多的第三方的依赖就选择了使用swift的版本的, 比如字典和模型的互转没有使用Mantle了, 取而代之的是使用了ObjectMapper, ObjectMapper的开发者为了更符合swift风格的编程, 没有在基于OC的运行时来实现了, 因为使用OC的运行时只能获取到继承自NSObject的class的属性的类型和值, 不能够获取到纯swift的class, struct, enum等的属性的类型和值了, 因为目前大家使用swift的时候更喜欢用struct来作为model, 所以基于运行时就不现实了, 不过带来的一点不方便就是: 需要手动的建立映射关系(这也有一个好处, 可以多个key映射json的同一个key), 当然随着swift的进步, 他的Reflect功能增强的话就可以方便的实现自动映射(虽然现在也可以实现, 不过不被推荐)
不过在使用上也是很简单的, 只需要这样, 如下调用这个map就将服务器返回的resultJson转换为了TagModel模型了
四. 网络请求的方面没有使用AFNetworking了, 而是使用出自同一个作者的Alamofire, 使用也是更加的简单和方便, 作者利用swift的优势使得Alamofire能让开发者更方便的实现各种需要的自定义配置
这里我只是简单的使用了GET和POST请求
/// get class func GET(URLString: String, parameters: [String: AnyObject]? = nil, successHandler:((result: AnyObject?) -> Void)?, failureHandler: ((error: NSError?) -> Void)?) { Alamofire.request(.GET, URLString, parameters: parameters, encoding: .URL, headers: nil).responseJSON { (response) in if response.result.isSuccess { print("初始请求:/(response.request)") successHandler?(result: response.result.value) } else { failureHandler?(error: response.result.error) } } } /// post class func POST(URLString: String, parameters: [String: AnyObject]? = nil, successHandler:((result: AnyObject?) -> Void)?, failureHandler: ((error: NSError?) -> Void)?) { Alamofire.request(.POST, URLString, parameters: parameters, encoding: .URL, headers: nil).responseJSON { (response) in if response.result.isSuccess { successHandler?(result: response.result.value) } else { failureHandler?(error: response.result.error) } } }}
如你所见, 使用就是如下的这么简单
五. 图片的加载方面没有使用SDWebimage, 而是使用了王巍的Kingfisher, 其中的接口设计以及原理和SDWebimage相类似, 所以你可以很快的就上手Kingfisher的使用了
/// 使用分类来加载图片, 同时提供进度和加载完成后的handler, 在这个handler里可以处理请求完成的图片 imageView.kf_setImageWithURL(NSURL(string: data.room_src)!, placeholderImage: nil, optionsInfo: nil, progressBlock: nil, completionHandler: nil) /// 先下载载设置图片 KingfisherManager.sharedManager.retrieveImageWithURL(NSURL(string: data.room_src)!, optionsInfo: nil, progressBlock: nil) {[weak self] (image, error, cacheType, imageURL) in guard let validSelf = self where image != nil else { return } validSelf.imageView.zj_setCircleImage(image, radius: 20.0) }
六. 自动布局上面没有使用masonry, 而是使用了同一个团队开发的SnapKit, 所以使用的方法几乎一样, 不过因为swift更适合函数式编程, 所以语法看上去也是自然了许多
七.关于RxSwift, 如果要使用MVVM的设计模式的话, 必须得解决view和viewModel的绑定问题, 那么最方便的就是使用第三方的响应式编程的框架, 这里推荐使用RxSwift, 这个学习的路线确实是很陡峭, 不是很容易就掌握了, 所以在项目中, 我只是在RecommendController简单的示例了一下RxSwift的使用, 另外RxSwift不单是方便MVVM, 更重要的是, 他把所有的(kvo, delegate, action- target, block, notification...)统一为了一种简单的使用方式, 真正的实现了高聚合, 低耦合. 同时RxSwift里面还有很多的用处, 比如实现搜索需求的时候, 需要在用户输入后实时的请求服务器, 这个时候, 就可以使用RxSwift和简单的实现, 在用户输入停留一段时间后请求服务器, 同时当输入的内容不变的时候不请求服务器... 总之很多的方便的功能, 绝对超乎你的想象, 等待你去发现...
八. 关于项目中文件的说明
main文件夹下主要是项目中通用的一些东西
MainNavigationController主要是用来统一配置项目中所有的Navigationtroller的一些属性, 比如在这个项目中, 我只是统一开启了全屏滑动返回的功能, 和拦截了弹出新控制器的方法, 你需要的各种其他自定义的, 建议也集中放在这里
class MainNavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() // 开启全屏pop手势 zj_enableFullScreenPop(true) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // 拦截 统一处理 override func showViewController(vc: UIViewController, sender: AnyObject?) { vc.hidesBottomBarWhenPushed = true super.showViewController(vc, sender: sender) } }
MainTabBarController 是用来统一处理项目中的Tabbarcontroller的一些属性, 当然很多人都是直接放在Appdelegate中来设置的, 个人还是喜欢全部分离开来
override func viewDidLoad() { super.viewDidLoad() /// 设置子控制器 setupChildVcs() /// 设置item的字体颜色 setTabBarItemColor() } func setTabBarItemColor() { UITabBarItem.appearance().setTitleTextAttributes([NSForegroundColorAttributeName: UIColor.orangeColor()], forState: .Selected) UITabBarItem.appearance().setTitleTextAttributes([NSForegroundColorAttributeName: UIColor.lightGrayColor()], forState: .Normal) } func setupChildVcs() { let homeVc = addChildVc(HomeController(), title: "首页", imageName: "btn_home_normal_24x24_", selectedImageName: "btn_home_selected_24x24_") let liveVc = addChildVc(LiveColumnController(), title: "直播", imageName: "btn_column_normal_24x24_", selectedImageName: "btn_column_selected_24x24_") let concernVc = addChildVc(ConcernController(), title: "关注", imageName: "btn_live_normal_30x24_", selectedImageName: "btn_live_selected_30x24_") let profileVc = addChildVc(ProfileController(), title: "我的", imageName: "btn_user_normal_24x24_", selectedImageName: "btn_user_selected_24x24_") viewControllers = [homeVc, liveVc, concernVc, profileVc] } func addChildVc(childVc: UIViewController, title: String, imageName: String, selectedImageName: String) -> UINavigationController { let navi = MainNavigationController(rootViewController: childVc) let image = UIImage(named: imageName)?.imageWithRenderingMode(.AlwaysOriginal) let selectedImage = UIImage(named: selectedImageName)?.imageWithRenderingMode(.AlwaysOriginal) let tabBarItem = UITabBarItem(title: title, image: image, selectedImage: selectedImage) navi.tabBarItem = tabBarItem return navi }
BaseViewController 是用来作为所有控制器的基类, 在里面统一处理一些设置, 在OC中, 我一般不喜欢使用基类来处理, 都是使用分类 +load()来统一设置一些, 比如设置view.backgroundColor, 但在swift中目前, mock不方便, 所以就使用了基类, 这也是很多朋友都喜欢使用的方式
class BaseViewController: UIViewController { /// 用于RxSwift var disposeBag = DisposeBag() /// 标记是否更新了布局 private var didUpdateConstraints = false override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.whiteColor() } /// 重写方法 override func updateViewConstraints() { if !didUpdateConstraints { addConstraints() didUpdateConstraints = true } super.updateViewConstraints() } /// 子类重写, 用于添加自动布局 func addConstraints() { /// default do nothing } }
lib文件夹下主要是使用的一些封装好的东西, 不过在这个项目中, lib里面的全是用的我自己写的一些东西, 一些之前已经放在了github上了, 这里简单介绍一下, 给自己一个广告??
FullScreenPopNavigationController -> 是为了方便navigationController实现全屏侧滑返回的功能的, 如你所见, 打开和关闭都只需一行代码// zj_enableFullScreenPop(true) (true)开启全屏pop手势, false关闭
ZJPullToRefresh -> 是我用swift写的一个和MJRefresh基本功能和使用相似的上下拉刷新控件
let normalAnimator = NormalAnimator.loadNormalAnimatorFromNib() normalAnimator.isAutomaticlyHidden = true normalAnimator.lastRefreshTimeKey = "recommondHeader" collectionView.zj_addRefreshHeader(normalAnimator) { [weak self] in /// 这里是加载过程 }
PPTView -> 是一个简单的图片轮播, 这个实现没什么难度, 可以使用链式调用, 几个链式调用的设置和tableView的几个代理方法的功能类似,在网络加载完毕的时候调用self.pptView.reloadData()可以像tableview一样重新加载数据
let pptView = PPTView.PPTViewWithImagesCount {[weak self] in guard let `self` = self else { return 0 } return self.viewModel.pptData.count } .setupImageAndTitle({[weak self] (titleLabel, imageView, index) in guard let `self` = self else { return } // let model = self.viewModel.pptData.value[index] let model = self.viewModel.pptData[index] titleLabel.textAlignment = .Left titleLabel.text = " " + "/(model.title)" imageView.image = UIImage(named: "2") imageView.kf_setImageWithURL(NSURL(string: model.pic_url), placeholderImage: UIImage(named: "1")) }) .setupPageDidClickAction({[weak self] (clickedIndex) in guard let `self` = self else { return } let playerVc = PlayerController() playerVc.title = "播放" playerVc.roomID = String(self.viewModel.pptData[clickedIndex].id) self.showViewController(playerVc, sender: nil) }) pptView.frame = CGRect(x: 0, y: 0, width: Constant.screenWidth, height: ConstantValue.pptViewHeight) pptView.pageControlPosition = .BottomRight return pptView
ScrollPageView -> 是用来实现类似网易新闻的头部标签栏等多种效果
TypedTableView -> 是简单封装了一下"静态"tableView的使用, 这个看个人的习惯
let row1Data = TypedCellDataModel(name: "开播提示", iconName: "1") let row2Data = TypedCellDataModel(name: "票务查询", iconName: "1") let row3Data = TypedCellDataModel(name: "设置选项", iconName: "1") let row4Data = TypedCellDataModel(name: "手游中心", iconName: "1", detailValue: "玩游戏领鱼丸") let row1 = CellBuilder(dataModel: row1Data, cellDidClickAction: { SimpleHUD.showHUD("未实现相关功能", autoHide: true, afterTime: 1.0) }) let row2 = CellBuilder(dataModel: row2Data, cellDidClickAction: { SimpleHUD.showHUD("未实现相关功能", autoHide: true, afterTime: 1.0) }) let row3 = CellBuilder(dataModel: row3Data, cellDidClickAction: {[unowned self] in self.showViewController(SettingController(), sender: nil) }) let row4 = CellBuilder(dataModel: row4Data, cellHeight: 50, cellDidClickAction: {[unowned self] in self.showViewController(TestController(), sender: nil) }) let section1 = CommonTableSectionData(headerTitle: nil, footerTitle: nil, headerHeight: 10, footerHeight: nil, rows: [row1, row2, row3]) let section2 = CommonTableSectionData(headerTitle: nil, footerTitle: nil, headerHeight: 10, footerHeight: 10, rows: [row4]) data = [section1, section2]
PhotoBrowser -> 图片浏览器, 可以支持浏览本地和网络的图片,很方便的简单的实现类似空间, 朋友圈动态的多张图片浏览, 已经写好各种手势放大缩小, 保存等常用功能, 本项目中只是简单的使用了, 浏览本地的图片
lazy var profileHeadView: ProfileHeadView = { let profileHeadView = ProfileHeadView.LoadProfileHeadViewFormLib() profileHeadView.didTapImageViewHandler = {[weak self] imageView in guard let `self` = self else { return } /// 弹出图片浏览器 let photoModel = PhotoModel(localImage: imageView.image, sourceImageView: nil) let photoBrowser = PhotoBrowser(photoModels: [photoModel]) photoBrowser.hideToolBar = true photoBrowser.show(inVc: self, beginPage: 0) } return profileHeadView }()
UsefulPickerView -> 简单方便的弹出城市选择, 日期选择, 单列, 多列选择的pickerView,
let row1 = CellBuilder(dataModel: row1Data, cellDidClickAction: { UsefulPickerView.showDatePicker(row1Data.name, doneAction: { (selectedDate) in EasyHUD.showHUD("提示时间是---/(selectedDate)", autoHide: true, afterTime: 1.0) }) }) let row2 = CellBuilder(dataModel: row2Data, cellDidClickAction: { UsefulPickerView.showSingleColPicker(row2Data.name, data: ["是", "否"], defaultSelectedIndex: 0, doneAction: { (selectedIndex, selectedValue) in EasyHUD.showHUD("选择了---/(selectedValue)", autoHide: true, afterTime: 1.0) }) })
感觉这篇文章已经很长了, 先就介绍到这里吧, 当然希望你也可以自己下载项目下来看看, 项目地址:https://github.com/jasnig/DouYuTVMutate