Swift開源專案-模仿今日頭條
說明
首先宣告,今日頭條是我經常用的 app 之一,模仿今日頭條也是因為感興趣,程式碼僅用於學習交流。對於專案中的資料介面都是通過 Charles 抓包獲得,基本每個介面都是有資料請求,不會抓包的朋友可以看我 這一篇文章。
專案中有的地方程式碼寫的不是很簡潔,畢竟自己能力有限,對 Swift 使用不是很熟練,還請各位朋友不喜勿噴。下面有專案的完整原始碼,喜歡的朋友可以下載下來,如果您感覺我寫的程式碼對您有所幫助,還請在 github 給個 star,非常感謝您的支援!~
對於程式碼中出現的問題,可以及時聯絡我,我會繼續修改。
環境設定
專案環境
- Xcode 9.2(低於這個版本會報錯)。
- Swift 4
- iOS 11.0
使用 cocoaPods 管理第三方庫, 如果電腦沒有安裝 cocoapods,請先安裝 cocoapods。安裝方式可參考:最新版 CocoaPods 的安裝流程
專案中使用到的第三方庫
- SnapKit: 佈局
- Kingfisher: 快取圖片
- SVProgressHUD:提示框
- FDFullscreenPopGesture:側滑
- Alamofire :網路請求
- SwiftyJSON:解析 json
- MJRefresh: 上拉重新整理和下拉重新整理
實現的功能
- 獲取今日頭條的介面
- 完成首頁的佈局和資料的顯示
- 實現首頁頂部導航欄滾動
- 新聞詳情介面簡單實現
- 點選遮蔽按鈕,彈出遮蔽檢視(座標有一些問題)
- 完成視訊介面頂部導航欄滾動
- 完成視訊介面佈局和資料獲取
- 使用者介面簡單實現
- 完成關注介面佈局和資料的獲取
- 完成關注介面,新增關注功能
- 完成搜尋功能
- 完成個人介面的佈局
- 完成設定介面的佈局
- 完成離線下載介面佈局
- 活動介面簡單實現
- 登入介面的簡單實現
- 啟動介面的簡單實現
資料請求
今日頭條的介面檔案請看: news.json,需要提前安裝 postman,然後把該檔案匯入到 postman 進行檢視,可以開啟谷歌瀏覽器,找到擴充套件程式,新增新的擴充套件,搜尋 postman。
下載地址請看 postman,下載完成後,直接拖入到谷歌瀏覽器的擴充套件程式介面即可。
首頁
1.首先,首頁的狀態列的顏色是白色,所以呼叫了下面的方法:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
但是,經過測試,上面的程式碼不起作用,對於 YMMineViewController.swift
上面的程式碼是起作用的。
唯一的區別是就是在 YMMineViewController.swift
中隱藏了導航條。所以經過查閱資料,得到下面的結論:
1.不管是呼叫了系統的
UINavigationController
還是使用自己繼承自UINavigationController
,如果navigationBar
沒有被隱藏的話,那麼導航控制器的rootController
以及它push
的控制器的preferredStatusBarStyle()
方法都不會被呼叫。
2.如果在當前控制器手動設定了navagationBar
的barStyle
為.Black
或者.Default
或者使用下面的程式碼手動設定:
// 方式1
navigationController?.setNavigationBarHidden(true, animated: false)
// 方式2
navigationController?.navigationBarHidden = true
那麼 preferredStatusBarStyle()
就會被正常呼叫了。
2.關於導航欄的 titleView
在首頁首頁頂部標題的時候,直接設定 titleView 的寬度為螢幕的寬,但是兩邊總是會留出 10 的間距,這個時候需要重寫父類的 setFrame 方法,在 OC 裡面可以使用下面的方法:
- (void)setFrame:(CGRect)frame
{
CGRect newFrame = CGRectMake(0, 0, SCREENW, 44);
[super setFrame:frame];
}
但是在 swift 中不能這樣寫,要使用下面的方式:
/// 重寫 frame
override var frame: CGRect {
didSet {
let newFrame = CGRectMake(0, 0, SCREENW, 44)
super.frame = newFrame
}
}
這樣設定,執行程式,發現 titleView 在螢幕兩邊不在留有間距。
3.子控制器
YMHomeTopicController.swift
作為 YMHomeViewController.swift
的子控制器,顯示新聞資料。
該類註冊了四種 cell,分別表示中間三張圖片,右邊一張圖片,中間一張大圖,中間一張視訊大圖,沒有圖片的情況。
以下是四種情況:
在 tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
中,根據不同情況對要顯示的 cell 進行判斷,顯示對應的 cell。
具體判斷情況請看 Model
裡的 YMNewsTopic.swift
類。
詳情有下面幾種方式:
為了實現簡單,這裡使用 webView
來實現
iOS 8 以後推出的專門負責轉場動畫的控制器。
在 Xcode 7 以上的版本中,UIPresentationController
有一個 bug,見下圖:
presentingViewController
會報一個野指標的錯誤,這是 Xcode 的 bug。
UIPresentationController
中有兩個方法可以佈局子檢視,分別是:
// 即將佈局轉場子檢視時呼叫
public func containerViewWillLayoutSubviews()
// 佈局完成轉場子檢視時呼叫
public func containerViewDidLayoutSubviews()
可以在兩個方法裡設定 UIPresentationController
的容器檢視 containerView
和 被展現的檢視 presentedView()
。
這個類主要作為四種類型 cell 的父類,主要定義了 標題、頭像、暱稱、評論、關閉按鈕。
顯示圖片交給其子類各自實現。
當時考慮過使用一個類來實現四種類型的 cell,但是經過測試,由於 cell 的重用機制,始終不能達到想要的結果,所以才分別建立了四種不同的 cell,使用一個類的方式是 YMTopicTableViewCell.swift
這個類,大家可以參考一下。
由於首頁的情況比較多,cell 的顯示比較複雜,而且今日頭條的介面也不是很規範,所以這幾個類實現起來比較麻煩,而且程式碼寫的不是很簡潔,用了很多 if
判斷,可能看起來不是很美觀。
判斷的情況與 YMNewsTopic.swift
類相同,具體請看 YMNewsTopic.swift
。
我覺得使用不同 cell 的情況還比較簡單理解。
如果各位朋友有什麼更好的實現方法,歡迎給我留言或『Pull Request』,非常感謝您的留言和建議。
這個類和和視訊頂部標題的類有些類似,對於資料和按鈕點選的回撥使用閉包的方式。而在視訊的標題 YMVideoTitleView.swift
裡使用代理來代替閉包,實現的功能是相同的,但是實現的方式不同,可以對比看一下。
對控制元件的佈局方式還是使用的 SnapKit
來進行佈局。
這個類裡需要首先從伺服器獲取標題資料,伺服器返回一個數組,根據這個陣列迴圈建立標題的 label
,然後設定好 label
的位置以及 scrollView
的 contentSize
。
標題 label 的點選通過新增手勢來實現監聽點選操作,titleLabelOnClick
為標題點選的方法,當點選的時候,根據索引進行相應的偏移,呼叫 adjustTitleOffSetToCurrentIndex
來改變 label 的位置。
在 adjustTitleOffSetToCurrentIndex(currentIndex: Int, oldIndex: Int)
方法裡,需要獲取之前點選 label 的索引以及剛剛點選 label 的索引,改變形變,計算當前的偏移量。
重寫 frame,來設定導航欄不再有兩邊的間距。請看具體程式碼 206 行。
這個類是我覺得最麻煩的一個類了,有很多種情況,所以判斷也比較多。
今日頭條返回的資料中,有這四個欄位,image_list
,middle_image
,large_image_list
,video_detail_info
,在 cell 裡面分別對應 YMHomeSmallCell.swift
,YMHomeMiddleCell.swift
,YMHomeLargeCell.swift
。
image_list
這是一個陣列,表示中間有三種圖的情況;
middle_image
這是一個字典,表示圖片在右側的情況;
large_image_list
這是一個陣列,表示中間是一張大圖;
video_detail_info
這是一個字典,表示是視訊,中間也用一張大圖表示,這種情況和大圖的情況基本相同,但是視訊中間多了播放按鈕。
還有最後一種情況就是沒有圖片的情況,比如置頂的專題,但是置頂的專題和上面在舉報按鈕的地方也有所區別,置頂的新聞沒有舉報按鈕,其他情況有舉報按鈕,需要根據 一個欄位 label
來進行判斷。
上面五種情況出現的依賴關係,也需要進行判斷,
下面說一下,具體的判斷過程:
image_list | middle_image | large_image_list | video_detail_info |
---|---|---|---|
nil | nil | nil | nil |
nil | 不為 nil | 不為 nil | nil |
nil | 不為 nil | nil | nil |
不為 nil | 不為 nil | 不為 nil | 不為 nil |
不為 nil | 不為 nil | 不為 nil | nil |
不為 nil | 不為 nil | nil | 不為 nil |
不為 nil | 不為 nil | nil | nil |
還有一些其他情況,比如有個資料裡沒有 image_list
這個欄位,這種情況我沒做判斷,一般程式崩潰都是因為這個原因。但是實際上,我是先判斷 image_list
是否有值,如果有值,則顯示三張圖片,如果為 nil
,再判斷 middle_image
的情況。
如果各位朋友有什麼更好的實現方法,歡迎給我留言或『Pull Request』,非常感謝您的留言和建議。
負責轉場動畫的代理
自定義轉場動畫需要整合兩個代理協議,分別是 UIViewControllerTransitioningDelegate
和 UIViewControllerAnimatedTransitioning
。
如果需要自定義轉場動畫,那麼所有的操作需要自己完成,系統不再處理。
UIViewControllerTransitioningDelegate
UIViewControllerTransitioningDelegate
共有五個代理方法,這裡我用到了三個代理方法,分別是:
// MARK: - UIViewControllerTransitioningDelegate
/**
告訴系統由哪個控制器來實現代理
- parameter presented: 被展現的檢視
- parameter presenting: 展現的檢視
- returns: YMPopPresentationController iOS 8 以後推出的專門負責轉場動畫的控制器
*/
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?
/**
告訴系統誰來負責 modal 的展現動畫
- parameter presented: 被展現的檢視
- parameter presenting: 展現的檢視
- returns: 由誰管理
*/
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
/**
告訴系統誰來負責 modal 的消失動畫
- parameter dismissed: 消失的控制器
- returns: 由誰管理
*/
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
UIViewControllerAnimatedTransitioning
用到了兩個代理方法:
/** 動畫時長*/
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
/** 負責轉場動畫的效果*/
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if isPresent {
// 展開
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
// 一定要將檢視新增到容器上
transitionContext.containerView()?.addSubview(toView!)
// 錨點
toView?.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
toView?.transform = CGAffineTransformMakeScale(0.0, 0.0)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options:
YMHomeShareView.swift
分享介面:
視訊
這個控制器主要顯示頂部導航標題和帖子控制器的一個容器。
頂部導航標題請看 YMVideoTitleView.swift
,帖子控制器請看 YMVideoTopicController.swift
。
使用的一個 tableView 實現。整合上拉重新整理和下拉重新整理。實現起來不難。
但是視訊播放麻煩一點。需要考慮 cell 的重用機制,由於今日頭條返回的資料是一個網址,並不是視訊的真實地址,試了一些方法,想把視訊的真實地址搞出來,但是沒有成功,也是我能力有限。搜易視訊播放暫時寫了一個地址,播放是這一個視訊。
cell的圖片是一個 button 的背景圖片,通過 button 的點選事件,來判斷此時是選中還是沒有選中。當點選圖片按鈕或是播放按鈕的時候,在這個按鈕上再建立一個 playerView,來播放視訊,具體類請看 YMPlayerView.swift
。
首先提前定義一個 cell,來儲存上一次點選的 cell,然後通過 YMPlayerView.swift
的回撥, 首先把上一次 cell 的狀態,恢復原狀,然後再在當前選中的 cell 上,進行新的設定,並新增一個 YMPlayerView
。
說明:視訊播放還是存在問題,後面有時間會優化。
關注
這個類是第三個主控制器,顯示關注介面。
這個介面建立了一個 tableView,並且註冊了三種不同的 cell,分別是 YMNewCareNoLoginCell.swift
,YMNewCareTopCell.swift
,以及 YMNewCareBottomCell.swift
,可分別開啟各自的檔案,進行檢視。
首先設定 UI,然後 setupRefresh()
是新增上拉和下拉重新整理,然後將 tableView 分成了上下兩組,上邊一組表示自己新增的關注內容,下邊一組表示未新增的關注內容,下面一組可以上拉載入更多內容。
今日頭條的接口裡有一個 concern_time
欄位,未新增關注之前,該值為 0
,當新增某一關注內容之後,該值變為一個關注的時間,單位是秒。由於今日頭條接口裡返回的資料是一個數組,即已關注和未關注的內容同時包含在一個數組中,所以可以根據這個引數來區分是已關注還是未關注。具體方法可以參考 setupRefresh
方法裡面呼叫的 loadNewConcernList
方法,以及 loadMoreConcernList
方法,這兩個方法實現了對已關注和未關注的拆分。
接口裡還有一個引數需要注意,就是 newly
這個欄位,對於剛剛關注的內容或是已關注的內容並沒有點選相應的 cell,跳轉到下一控制器的關注內容,都會在右邊顯示一個 『NEW』,這個控制元件的顯示與隱藏需要根據 newly
的值進行判斷,newly
會返回兩種情況,一種是 1
, 另一種是 0
,即對應顯示和隱藏。
上面的引數定義請看 YMConcern.swift
.
在下面一組每一個 cell 上都有一個『關注』 按鈕,這裡我使用代理來實現按鈕點選的響應事件,讓 YMNewCareViewController
來接收按鈕的點選。
當新增關注的時候,會有一個動畫效果,這個動畫效果暫時還未實現,大家可以參考今日頭條的效果。參考一下,有實現的朋友,也可以聯絡我,也可以給我 『pull request』。
這個介面的實現還算簡單,就說明到這裡吧~
YMSearchContentViewController.swift
搜尋介面
點選關注介面的某一個 cell 之後,跳轉到下移控制器的頂部檢視,有一個模糊效果。
介面實現不算太難,主要是對每個控制元件的佈局,使用 SnapKit
。點選按鈕的回撥使用委託實現。
程式碼中註釋比較詳細,這裡不再說明。
具體程式碼請看 YMCareheaderView.swift
。
我的
隱藏導航欄的方法如下:
// 方式1
navigationController?.setNavigationBarHidden(true, animated: false)
// 方式2
navigationController?.navigationBarHidden = true
需要注意一點,隱藏導航欄的屬性寫到 viewDidLoad()
裡不起作用。
YMSettingViewController.swift
從檔案載入 cell 的資料,使用通知的方式,實現了清除快取,以及改變字型大小,改變下載方式。
YMOfflineTableViewController.swift
我的 -> 離線 -> 離線下載
對於標題的選中與未選中,使用歸檔的方式,YMHomeTopTitle
附加一個欄位來判斷選中與未選中,然後儲存到沙盒中,具體實現可看程式碼。
YMActivityController.swift
活動介面
為了實現簡單,使用一個 webView
實現。
登入和啟動介面
只是簡單的搭了一個介面,具體邏輯沒有實現: