授權轉載,作者: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