1. 程式人生 > >Swift開源專案-模仿今日頭條

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: 上拉重新整理和下拉重新整理

實現的功能

  1. 獲取今日頭條的介面
  2. 完成首頁的佈局和資料的顯示
  3. 實現首頁頂部導航欄滾動
  4. 新聞詳情介面簡單實現
  5. 點選遮蔽按鈕,彈出遮蔽檢視(座標有一些問題)
  6. 完成視訊介面頂部導航欄滾動
  7. 完成視訊介面佈局和資料獲取
  8. 使用者介面簡單實現
  9. 完成關注介面佈局和資料的獲取
  10. 完成關注介面,新增關注功能
  11. 完成搜尋功能
  12. 完成個人介面的佈局
  13. 完成設定介面的佈局
  14. 完成離線下載介面佈局
  15. 活動介面簡單實現
  16. 登入介面的簡單實現
  17. 啟動介面的簡單實現

資料請求

今日頭條的介面檔案請看: news.json,需要提前安裝 postman,然後把該檔案匯入到 postman 進行檢視,可以開啟谷歌瀏覽器,找到擴充套件程式,新增新的擴充套件,搜尋 postman。

下載地址請看 postman,下載完成後,直接拖入到谷歌瀏覽器的擴充套件程式介面即可。

首頁

首頁-1

1.首先,首頁的狀態列的顏色是白色,所以呼叫了下面的方法:

override func preferredStatusBarStyle() -> UIStatusBarStyle {
    return .LightContent
}

但是,經過測試,上面的程式碼不起作用,對於 YMMineViewController.swift 上面的程式碼是起作用的。

唯一的區別是就是在 YMMineViewController.swift 中隱藏了導航條。所以經過查閱資料,得到下面的結論:

1.不管是呼叫了系統的 UINavigationController 還是使用自己繼承自 UINavigationController,如果 navigationBar 沒有被隱藏的話,那麼導航控制器的 rootController 以及它 push 的控制器的 preferredStatusBarStyle() 方法都不會被呼叫。
2.如果在當前控制器手動設定了 navagationBarbarStyle.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()

containerView-presentedView

這個類主要作為四種類型 cell 的父類,主要定義了 標題、頭像、暱稱、評論、關閉按鈕。

顯示圖片交給其子類各自實現。

當時考慮過使用一個類來實現四種類型的 cell,但是經過測試,由於 cell 的重用機制,始終不能達到想要的結果,所以才分別建立了四種不同的 cell,使用一個類的方式是 YMTopicTableViewCell.swift 這個類,大家可以參考一下。

由於首頁的情況比較多,cell 的顯示比較複雜,而且今日頭條的介面也不是很規範,所以這幾個類實現起來比較麻煩,而且程式碼寫的不是很簡潔,用了很多 if 判斷,可能看起來不是很美觀。

判斷的情況與 YMNewsTopic.swift 類相同,具體請看 YMNewsTopic.swift

我覺得使用不同 cell 的情況還比較簡單理解。

如果各位朋友有什麼更好的實現方法,歡迎給我留言或『Pull Request』,非常感謝您的留言和建議。

這個類和和視訊頂部標題的類有些類似,對於資料和按鈕點選的回撥使用閉包的方式。而在視訊的標題 YMVideoTitleView.swift 裡使用代理來代替閉包,實現的功能是相同的,但是實現的方式不同,可以對比看一下。

對控制元件的佈局方式還是使用的 SnapKit 來進行佈局。

這個類裡需要首先從伺服器獲取標題資料,伺服器返回一個數組,根據這個陣列迴圈建立標題的 label,然後設定好 label 的位置以及 scrollViewcontentSize

標題 label 的點選通過新增手勢來實現監聽點選操作,titleLabelOnClick 為標題點選的方法,當點選的時候,根據索引進行相應的偏移,呼叫 adjustTitleOffSetToCurrentIndex 來改變 label 的位置。

adjustTitleOffSetToCurrentIndex(currentIndex: Int, oldIndex: Int) 方法裡,需要獲取之前點選 label 的索引以及剛剛點選 label 的索引,改變形變,計算當前的偏移量。

重寫 frame,來設定導航欄不再有兩邊的間距。請看具體程式碼 206 行。

這個類是我覺得最麻煩的一個類了,有很多種情況,所以判斷也比較多。

今日頭條返回的資料中,有這四個欄位,image_listmiddle_imagelarge_image_listvideo_detail_info,在 cell 裡面分別對應 YMHomeSmallCell.swiftYMHomeMiddleCell.swiftYMHomeLargeCell.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』,非常感謝您的留言和建議。

負責轉場動畫的代理

自定義轉場動畫需要整合兩個代理協議,分別是 UIViewControllerTransitioningDelegateUIViewControllerAnimatedTransitioning

如果需要自定義轉場動畫,那麼所有的操作需要自己完成,系統不再處理。

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.swiftYMNewCareTopCell.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 實現。

登入和啟動介面

只是簡單的搭了一個介面,具體邏輯沒有實現:

登入

啟動