转载

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

授权转载,作者:ZeroJ

前言:

之前闲着的时候就随便模仿斗鱼的界面写了一些界面, 最初的时候在网上找到的获取直播的sign加密方式还是可用的, 当时还使用IJKMediaFramework, 集成了直播视频的获取和播放, 当时的项目也就还是挺庞大的, 不过大约在7.21 左右斗鱼的api升级了, 然后就不能获取到直播了, 所以现在把项目中的直播相关的全部都删除了。

目前项目中就只能看到部分的界面和一些网络的请求了, 项目是使用swift来实现的, 但是如果你是最初接触swift的话, 有一些地方可能可以参考一下. 项目地址

一些页面的效果如下

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

关于项目的一些解释

一. 最初是使用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的变动, 就全部给移除了

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

三. 项目使用纯swift写的, 所以很多的第三方的依赖就选择了使用swift的版本的, 比如字典和模型的互转没有使用Mantle了, 取而代之的是使用了ObjectMapper, ObjectMapper的开发者为了更符合swift风格的编程, 没有在基于OC的运行时来实现了, 因为使用OC的运行时只能获取到继承自NSObject的class的属性的类型和值, 不能够获取到纯swift的class, struct, enum等的属性的类型和值了, 因为目前大家使用swift的时候更喜欢用struct来作为model, 所以基于运行时就不现实了, 不过带来的一点不方便就是: 需要手动的建立映射关系(这也有一个好处, 可以多个key映射json的同一个key), 当然随着swift的进步, 他的Reflect功能增强的话就可以方便的实现自动映射(虽然现在也可以实现, 不过不被推荐)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

不过在使用上也是很简单的, 只需要这样, 如下调用这个map就将服务器返回的resultJson转换为了TagModel模型了

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

四. 网络请求的方面没有使用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)
            }
        }
    }}

如你所见, 使用就是如下的这么简单

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

五. 图片的加载方面没有使用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)

七.关于RxSwift, 如果要使用MVVM的设计模式的话, 必须得解决view和viewModel的绑定问题, 那么最方便的就是使用第三方的响应式编程的框架, 这里推荐使用RxSwift, 这个学习的路线确实是很陡峭, 不是很容易就掌握了, 所以在项目中, 我只是在RecommendController简单的示例了一下RxSwift的使用, 另外RxSwift不单是方便MVVM, 更重要的是, 他把所有的(kvo, delegate, action- target, block, notification...)统一为了一种简单的使用方式, 真正的实现了高聚合, 低耦合. 同时RxSwift里面还有很多的用处, 比如实现搜索需求的时候, 需要在用户输入后实时的请求服务器, 这个时候, 就可以使用RxSwift和简单的实现, 在用户输入停留一段时间后请求服务器, 同时当输入的内容不变的时候不请求服务器... 总之很多的方便的功能, 绝对超乎你的想象, 等待你去发现...

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

八. 关于项目中文件的说明

main文件夹下主要是项目中通用的一些东西

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

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关闭

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

ZJPullToRefresh -> 是我用swift写的一个和MJRefresh基本功能和使用相似的上下拉刷新控件

let normalAnimator = NormalAnimator.loadNormalAnimatorFromNib()
normalAnimator.isAutomaticlyHidden = true
normalAnimator.lastRefreshTimeKey = "recommondHeader"
collectionView.zj_addRefreshHeader(normalAnimator) { [weak self] in
 /// 这里是加载过程
}

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

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

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

ScrollPageView -> 是用来实现类似网易新闻的头部标签栏等多种效果

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

TypedTableView -> 是简单封装了一下"静态"tableView的使用, 这个看个人的习惯

模仿斗鱼的部分界面介绍一(部分使用RxSwift, MVVM)

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

正文到此结束
Loading...