前言:
之前闲着的时候就随便模仿斗鱼的界面写了一些界面, 最初的时候在网上找到的获取直播的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 /// 这里是加载过程 } ```
-
FullScreenPopNavigationController -> 是为了方便navigationController实现全屏侧滑返回的功能的, 如你所见, 打开和关闭都只需一行代码
* 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<TitleWithLeftImageCell>(dataModel: row1Data, cellDidClickAction: {
SimpleHUD.showHUD("未实现相关功能", autoHide: true, afterTime: 1.0)
})
let row2 = CellBuilder<TitleWithLeftImageCell>(dataModel: row2Data, cellDidClickAction: {
SimpleHUD.showHUD("未实现相关功能", autoHide: true, afterTime: 1.0)
})
let row3 = CellBuilder<TitleWithLeftImageCell>(dataModel: row3Data, cellDidClickAction: {[unowned self] in
self.showViewController(SettingController(), sender: nil)
})
let row4 = CellBuilder<TitleWithLeftImageAndDetailCell>(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<TitleWithLeftImageCell>(dataModel: row1Data, cellDidClickAction: {
UsefulPickerView.showDatePicker(row1Data.name, doneAction: { (selectedDate) in
EasyHUD.showHUD("提示时间是---\(selectedDate)", autoHide: true, afterTime: 1.0)
})
})
let row2 = CellBuilder<TitleWithLeftImageCell>(dataModel: row2Data, cellDidClickAction: {
UsefulPickerView.showSingleColPicker(row2Data.name, data: ["是", "否"], defaultSelectedIndex: 0, doneAction: { (selectedIndex, selectedValue) in
EasyHUD.showHUD("选择了---\(selectedValue)", autoHide: true, afterTime: 1.0)
})
})
感觉这篇文章已经很长了, 先就介绍到这里吧, 当然希望你也可以自己下载项目下来看看, 项目地址