前言:
之前閑著的時候就隨便模仿斗魚的界面寫了一些界面, 最初的時候在網(wǎng)上找到的獲取直播的sign加密方式還是可用的, 當時還使用IJKMediaFramework, 集成了直播視頻的獲取和播放, 當時的項目也就還是挺龐大的, 不過大約在7.21 左右斗魚的api升級了, 然后就不能獲取到直播了, 所以現(xiàn)在把項目中的直播相關(guān)的全部都刪除了
目前項目中就只能看到部分的界面和一些網(wǎng)絡(luò)的請求了, 項目是使用swift來實現(xiàn)的, 但是如果你是最初接觸swift的話, 有一些地方可能可以參考一下. 項目地址
一些頁面的效果如下
關(guān)于項目的一些解釋
一. 最初是使用MVC來設(shè)計的項目的, 最近開始接觸MVVM設(shè)計模式,在網(wǎng)上找到的各種MVVM的相關(guān)的資料, 就把先前的這個項目拿來改動試試, 然后在改的時候發(fā)現(xiàn), 很多時候不可能做到理想的MVVM架構(gòu)的, 因為可能使用到第三方的東西導(dǎo)致不能很方便的使用MVVM, 另外就是, 個人覺得簡單的界面使用MVVM就是在浪費時間
這里關(guān)于MVVM
就簡單的提一下了
- MVVM = model, view(viewController), viewModel
- 在MVVM中, 每個view(viewController)理論上對應(yīng)一個viewModel, view(viewController)負責界面的布局, 和響應(yīng)用戶的點擊, 以及展示頁面...
- viewModel用于處理view的所有的展示邏輯(請求網(wǎng)絡(luò), 操作數(shù)據(jù)庫, 格式化字符串...), 而且完美的viewModel里面是不應(yīng)該引入UIKit的, 所以viewModel就擁有view所需要的所有的數(shù)據(jù), viewModel中只進行數(shù)據(jù)的加工, 能夠?qū)@些數(shù)據(jù)進行必要的操作, 然后讓對應(yīng)的view更新數(shù)據(jù).
- 因為view是擁有viewModel的, 所以要實現(xiàn)view和viewModel的通信(view更新的時候同步更新viewModel中的數(shù)據(jù))很簡單, 但是要實現(xiàn)viewModel和view的綁定就很難得, 有時候你可以選擇(kvo, 代理, 通知, block...), 但是很多時候?qū)崿F(xiàn)都是非常的麻煩的, 因為你需要做到在viewModel中更新的時候
同步
更新對應(yīng)的view的狀態(tài). - 所以這個時候你就需要一個響應(yīng)式編程的框架,來實現(xiàn)view和viewModel的(單)雙向綁定, 比如OC中你可以用ReactiveCocoa, 在swift中, 你可以使用ReactiveCocoa, RxSwift, Bond...(推薦RxSwift, 號稱是符合RX官方的設(shè)計, 跨平臺的設(shè)計理念, RxJava, RxJS...可以類似的使用)
- 另外有人提出更符合MVVM的是viewModel只暴露一些輸入和輸出
信號
給view, 通過將這些信號綁定到view上面實現(xiàn)和view的同步更新, 而viewModel不暴露方法給view, 比如按鈕的點擊和viewModel的一個按鈕點擊的信號綁定, 在viewModel中通過訂閱這個信號處理按鈕的點擊, 而不是在view中調(diào)用viewModel的響應(yīng)按鈕點擊的方法... 不過個人更傾向于暴露方法, 因為感覺使用信號的話對第三方的框架依賴太大了 - model和MVC中的model基本相似的角色, 這里就不介紹了, 關(guān)于MVVM的更多的介紹, 推薦看這一系列的博客
二. 項目最初是集成了IJKMediaFramework并且實現(xiàn)了直播的一些功能, 不過由于斗魚Api的變動, 就全部給移除了
三. 項目使用純swift寫的, 所以很多的第三方的依賴就選擇了使用swift的版本的, 比如字典和模型的互轉(zhuǎn)沒有使用Mantle了, 取而代之的是使用了ObjectMapper, ObjectMapper的開發(fā)者為了更符合swift風格的編程, 沒有在基于OC的運行時來實現(xiàn)了, 因為使用OC的運行時只能獲取到繼承自NSObject的class的屬性的類型和值, 不能夠獲取到純swift的class, struct, enum等的屬性的類型和值了, 因為目前大家使用swift的時候更喜歡用struct來作為model, 所以基于運行時就不現(xiàn)實了, 不過帶來的一點不方便就是: 需要手動的建立映射關(guān)系(這也有一個好處, 可以多個key映射json的同一個key), 當然隨著swift的進步, 他的Reflect功能增強的話就可以方便的實現(xiàn)自動映射(雖然現(xiàn)在也可以實現(xiàn), 不過不被推薦)
不過在使用上也是很簡單的, 只需要這樣, 如下調(diào)用這個map就將服務(wù)器返回的resultJson轉(zhuǎn)換為了TagModel模型了
四. 網(wǎng)絡(luò)請求的方面沒有使用AFNetworking
了, 而是使用出自同一個作者的Alamofire, 使用也是更加的簡單和方便, 作者利用swift的優(yōu)勢使得Alamofire能讓開發(fā)者更方便的實現(xiàn)各種需要的自定義配置
這里我只是簡單的使用了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, 其中的接口設(shè)計以及原理和SDWebimage相類似, 所以你可以很快的就上手Kingfisher的使用了
/// 使用分類來加載圖片, 同時提供進度和加載完成后的handler, 在這個handler里可以處理請求完成的圖片
imageView.kf_setImageWithURL(NSURL(string: data.room_src)!, placeholderImage: nil, optionsInfo: nil, progressBlock: nil, completionHandler: nil)
/// 先下載載設(shè)置圖片
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, 而是使用了同一個團隊開發(fā)的SnapKit, 所以使用的方法幾乎一樣, 不過因為swift更適合函數(shù)式編程, 所以語法看上去也是自然了許多
七.關(guān)于RxSwift, 如果要使用MVVM的設(shè)計模式的話, 必須得解決view和viewModel的綁定問題, 那么最方便的就是使用第三方的響應(yīng)式編程的框架, 這里推薦使用RxSwift, 這個學(xué)習的路線確實是很陡峭, 不是很容易就掌握了, 所以在項目中, 我只是在RecommendController簡單的示例了一下RxSwift的使用, 另外RxSwift不單是方便MVVM, 更重要的是, 他把所有的(kvo, delegate, action- target, block, notification...)統(tǒng)一為了一種簡單的使用方式, 真正的實現(xiàn)了高聚合, 低耦合. 同時RxSwift里面還有很多的用處, 比如實現(xiàn)搜索需求的時候, 需要在用戶輸入后實時的請求服務(wù)器, 這個時候, 就可以使用RxSwift和簡單的實現(xiàn), 在用戶輸入停留一段時間后請求服務(wù)器, 同時當輸入的內(nèi)容不變的時候不請求服務(wù)器... 總之很多的方便的功能, 絕對超乎你的想象, 等待你去發(fā)現(xiàn)...
八. 關(guān)于項目中文件的說明
- main文件夾下主要是項目中通用的一些東西
- MainNavigationController主要是用來統(tǒng)一配置項目中所有的Navigationtroller的一些屬性, 比如在這個項目中, 我只是統(tǒng)一開啟了全屏滑動返回的功能, 和攔截了彈出新控制器的方法, 你需要的各種其他自定義的, 建議也集中放在這里
class MainNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// 開啟全屏pop手勢
zj_enableFullScreenPop(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// 攔截 統(tǒng)一處理
override func showViewController(vc: UIViewController, sender: AnyObject?) {
vc.hidesBottomBarWhenPushed = true
super.showViewController(vc, sender: sender)
}
}
- MainTabBarController 是用來統(tǒng)一處理項目中的Tabbarcontroller的一些屬性, 當然很多人都是直接放在Appdelegate中來設(shè)置的, 個人還是喜歡全部分離開來
override func viewDidLoad() {
super.viewDidLoad()
/// 設(shè)置子控制器
setupChildVcs()
/// 設(shè)置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: "關(guān)注", 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 是用來作為所有控制器的基類, 在里面統(tǒng)一處理一些設(shè)置, 在OC中, 我一般不喜歡使用基類來處理, 都是使用分類 +load()來統(tǒng)一設(shè)置一些, 比如設(shè)置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里面的全是用的我自己寫的一些東西, 一些之前已經(jīng)放在了github上了, 這里簡單介紹一下, 給自己一個廣告??
-
FullScreenPopNavigationController -> 是為了方便navigationController實現(xiàn)全屏側(cè)滑返回的功能的, 如你所見, 打開和關(guān)閉都只需一行代碼
// zj_enableFullScreenPop(true) (true)開啟全屏pop手勢, false關(guān)閉
Snip20160808_16.png -
ZJPullToRefresh -> 是我用swift寫的一個和MJRefresh基本功能和使用相似的上下拉刷新控件
let normalAnimator = NormalAnimator.loadNormalAnimatorFromNib() normalAnimator.isAutomaticlyHidden = true normalAnimator.lastRefreshTimeKey = "recommondHeader" collectionView.zj_addRefreshHeader(normalAnimator) { [weak self] in /// 這里是加載過程 } ```
-
FullScreenPopNavigationController -> 是為了方便navigationController實現(xiàn)全屏側(cè)滑返回的功能的, 如你所見, 打開和關(guān)閉都只需一行代碼
* PPTView -> 是一個簡單的圖片輪播, 這個實現(xiàn)沒什么難度, 可以使用鏈式調(diào)用, 幾個鏈式調(diào)用的設(shè)置和tableView的幾個代理方法的功能類似,在網(wǎng)絡(luò)加載完畢的時候調(diào)用
self.pptView.reloadData()
可以像tableview一樣重新加載數(shù)據(jù)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 -> 是用來實現(xiàn)類似網(wǎng)易新聞的頭部標簽欄等多種效果
- TypedTableView -> 是簡單封裝了一下"靜態(tài)"tableView的使用, 這個看個人的習慣
let row1Data = TypedCellDataModel(name: "開播提示", iconName: "1")
let row2Data = TypedCellDataModel(name: "票務(wù)查詢", iconName: "1")
let row3Data = TypedCellDataModel(name: "設(shè)置選項", iconName: "1")
let row4Data = TypedCellDataModel(name: "手游中心", iconName: "1", detailValue: "玩游戲領(lǐng)魚丸")
let row1 = CellBuilder<TitleWithLeftImageCell>(dataModel: row1Data, cellDidClickAction: {
SimpleHUD.showHUD("未實現(xiàn)相關(guān)功能", autoHide: true, afterTime: 1.0)
})
let row2 = CellBuilder<TitleWithLeftImageCell>(dataModel: row2Data, cellDidClickAction: {
SimpleHUD.showHUD("未實現(xiàn)相關(guān)功能", 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 -> 圖片瀏覽器, 可以支持瀏覽本地和網(wǎng)絡(luò)的圖片,很方便的簡單的實現(xiàn)類似空間, 朋友圈動態(tài)的多張圖片瀏覽, 已經(jīng)寫好各種手勢放大縮小, 保存等常用功能, 本項目中只是簡單的使用了, 瀏覽本地的圖片
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)
})
})
感覺這篇文章已經(jīng)很長了, 先就介紹到這里吧, 當然希望你也可以自己下載項目下來看看, 項目地址