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

前言:

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

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

一些页面的效果如下
douyu1.gif
douyu2.gif
douyu3.gif
douyu4.gif
douyu5.gif
douyu6.gif
douyu7.gif
关于项目的一些解释

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

Snip20160808_1.png

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

Snip20160808_2.png

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

Snip20160808_3.png

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

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

use

五. 图片的加载方面没有使用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更适合函数式编程, 所以语法看上去也是自然了许多

Snip20160808_6.png

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

rxSwift1
rxSwift2

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

  • main文件夹下主要是项目中通用的一些东西
Snip20160808_23.png
  • 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关闭
      Snip20160808_16.png
    • ZJPullToRefresh -> 是我用swift写的一个和MJRefresh基本功能和使用相似的上下拉刷新控件
     let normalAnimator = NormalAnimator.loadNormalAnimatorFromNib()
      normalAnimator.isAutomaticlyHidden = true
      normalAnimator.lastRefreshTimeKey = "recommondHeader"
      collectionView.zj_addRefreshHeader(normalAnimator) { [weak self] in
        /// 这里是加载过程
      }
      ```
    

Snip20160808_14.png

* 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
Snip20160808_15.png

  • ScrollPageView -> 是用来实现类似网易新闻的头部标签栏等多种效果
Snip20160808_13.png
  • TypedTableView -> 是简单封装了一下"静态"tableView的使用, 这个看个人的习惯
Snip20160808_17.png

        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)
                
            })

        })

感觉这篇文章已经很长了, 先就介绍到这里吧, 当然希望你也可以自己下载项目下来看看, 项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容