前言:
- 最近抽空面了幾家公司,大部分都是從基礎開始慢慢深入專案和原理。面試內容還是以
OC
為主,但是多數也都會問一下Swift
技術情況,也有例外全程問Swift
的公司(做區塊鏈專案),感覺現在雖然大多數公司任然以OC
做為主開發語言,但是Swift
發展很強勢,估計明年Swift5
以後使用會更加廣泛。- 另外,如果準備跳槽的話,可以提前投簡歷抽空面試幾家公司,一方面可以通過投遞反饋檢驗簡歷,另外可以總結面試的大致問題方向有利於做針對性複習,畢竟會用也要會說才行,會說也要能說到重點才行,還有就是心儀的公司一定要留到最後面試。希望都能進一個心儀不坑的公司,當然也應努力提升自己的技術,不坑公司不坑團隊, 好像跑題了!!!
目錄:
- 上一個仿寫專案
GitHub
:github.com/daomoer/YYS… 專案分析地址:Swift仿寫有妖氣漫畫- 本專案開始前準備階段:Swift高仿喜馬拉雅APP之一Charles抓包、圖片資源獲取等
- 本專案
GitHub
:github.com/daomoer/XML…
關於專案:
該專案採用MVC
+MVVM
設計模式,Moya
+SwiftyJSON
+HandyJSON
網路框架和資料解析。資料來源抓包及部分本地json
檔案。
使用Xcode9.4
基於Swift4.1
進行開發。
專案中使用到的一些開源庫以下列表,在這裡感謝作者的開源。
pod 'SnapKit'
pod 'Kingfisher'
#tabbar樣式
pod 'ESTabBarController-swift'
#banner滾動圖片
pod 'FSPagerView'
pod 'Moya'
pod 'HandyJSON'
pod 'SwiftyJSON'
# 分頁
pod 'DNSPageView'
#跑馬燈
pod 'JXMarqueeView'
#滾動頁
pod 'LTScrollView'
#重新整理
pod 'MJRefresh'
#訊息提示
pod 'SwiftMessages'
pod 'SVProgressHUD'
#播放網路音訊
pod 'StreamingKit'
複製程式碼
效果圖:
專案按照MVVM
模式進行設計,下面貼一下ViewModel
中介面請求和佈局設定方法程式碼。
import UIKit
import SwiftyJSON
import HandyJSON
class HomeRecommendViewModel: NSObject {
// MARK - 資料模型
var fmhomeRecommendModel:FMHomeRecommendModel?
var homeRecommendList:[HomeRecommendModel]?
var recommendList : [RecommendListModel]?
// Mark: -資料來源更新
typealias AddDataBlock = () ->Void
var updataBlock:AddDataBlock?
// Mark:-請求資料
extension HomeRecommendViewModel {
func refreshDataSource() {
//首頁推薦介面請求
FMRecommendProvider.request(.recommendList) { result in
if case let .success(response) = result {
//解析資料
let data = try? response.mapJSON()
let json = JSON(data!)
if let mappedObject = JSONDeserializer<FMHomeRecommendModel>.deserializeFrom(json: json.description) { // 從字串轉換為物件例項
self.fmhomeRecommendModel = mappedObject
self.homeRecommendList = mappedObject.list
if let recommendList = JSONDeserializer<RecommendListModel>.deserializeModelArrayFrom(json: json["list"].description) {
self.recommendList = recommendList as? [RecommendListModel]
}
}
}
}
// Mark:-collectionview資料
extension HomeRecommendViewModel {
func numberOfSections(collectionView:UICollectionView) ->Int {
return (self.homeRecommendList?.count) ?? 0
}
// 每個分割槽顯示item數量
func numberOfItemsIn(section: NSInteger) -> NSInteger {
return 1
}
//每個分割槽的內邊距
func insetForSectionAt(section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
}
//最小 item 間距
func minimumInteritemSpacingForSectionAt(section:Int) ->CGFloat {
return 0
}
//最小行間距
func minimumLineSpacingForSectionAt(section:Int) ->CGFloat {
return 0
}
// 分割槽頭檢視size
func referenceSizeForHeaderInSection(section: Int) -> CGSize {
let moduleType = self.homeRecommendList?[section].moduleType
if moduleType == "focus" || moduleType == "square" || moduleType == "topBuzz" || moduleType == "ad" || section == 18 {
return CGSize.zero
}else {
return CGSize.init(width: YYScreenHeigth, height:40)
}
}
// 分割槽尾檢視size
func referenceSizeForFooterInSection(section: Int) -> CGSize {
let moduleType = self.homeRecommendList?[section].moduleType
if moduleType == "focus" || moduleType == "square" {
return CGSize.zero
}else {
return CGSize.init(width: YYScreenWidth, height: 10.0)
}
}
}
複製程式碼
與ViewModel
相對應的是控制器Controller.m
檔案中的使用,使用MVVM
可以梳理Controller
看起來更整潔一點,避免滿眼的邏輯判斷。
lazy var viewModel: HomeRecommendViewModel = {
return HomeRecommendViewModel()
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.collectionView)
self.collectionView.snp.makeConstraints { (make) in
make.width.height.equalToSuperview()
make.center.equalToSuperview()
}
self.collectionView.uHead.beginRefreshing()
loadData()
loadRecommendAdData()
}
func loadData(){
// 載入資料
viewModel.updataBlock = { [unowned self] in
self.collectionView.uHead.endRefreshing()
// 更新列表資料
self.collectionView.reloadData()
}
viewModel.refreshDataSource()
}
// MARK - collectionDelegate
extension HomeRecommendController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModel.numberOfSections(collectionView:collectionView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.numberOfItemsIn(section: section)
}
//每個分割槽的內邊距
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return viewModel.insetForSectionAt(section: section)
}
//最小 item 間距
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return viewModel.minimumInteritemSpacingForSectionAt(section: section)
}
//最小行間距
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return viewModel.minimumLineSpacingForSectionAt(section: section)
}
//item 的尺寸
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return viewModel.sizeForItemAt(indexPath: indexPath)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return viewModel.referenceSizeForHeaderInSection(section: section)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return viewModel.referenceSizeForFooterInSection(section: section)
}
複製程式碼
首頁模組分析:
專案首頁推薦模組,根據介面請求資料進行處理,頂部的Banner
滾動圖片和分類按鈕以及下面的聽頭條統一劃分為HeaderCell
,在這個HeaderCell
中繼續劃分,頂部Banner
單獨處理,下面建立CollectionView
,並把分類按鈕和聽頭條作為兩個Section
,其中聽頭條的實現思路為CollectionCell
,通過定時器控制器自動上下滾動。
moduleType
進行Section
初始化並返回不同樣式的Cell
,另外在該模組中還穿插有廣告,廣告為單獨介面,根據介面返回資料穿插到對應的Section
。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let moduleType = viewModel.homeRecommendList?[indexPath.section].moduleType
if moduleType == "focus" || moduleType == "square" || moduleType == "topBuzz" {
let cell:FMRecommendHeaderCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendHeaderCellID, for: indexPath) as! FMRecommendHeaderCell
cell.focusModel = viewModel.focus
cell.squareList = viewModel.squareList
cell.topBuzzListData = viewModel.topBuzzList
cell.delegate = self
return cell
}else if moduleType == "guessYouLike" || moduleType == "paidCategory" || moduleType == "categoriesForLong" || moduleType == "cityCategory"{
///橫式排列布局cell
let cell:FMRecommendGuessLikeCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendGuessLikeCellID, for: indexPath) as! FMRecommendGuessLikeCell
cell.delegate = self
cell.recommendListData = viewModel.homeRecommendList?[indexPath.section].list
return cell
}else if moduleType == "categoriesForShort" || moduleType == "playlist" || moduleType == "categoriesForExplore"{
// 豎式排列布局cell
let cell:FMHotAudiobookCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMHotAudiobookCellID, for: indexPath) as! FMHotAudiobookCell
cell.delegate = self
cell.recommendListData = viewModel.homeRecommendList?[indexPath.section].list
return cell
}else if moduleType == "ad" {
let cell:FMAdvertCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMAdvertCellID, for: indexPath) as! FMAdvertCell
if indexPath.section == 7 {
cell.adModel = self.recommnedAdvertList?[0]
}else if indexPath.section == 13 {
cell.adModel = self.recommnedAdvertList?[1]
}
return cell
}else if moduleType == "oneKeyListen" {
let cell:FMOneKeyListenCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMOneKeyListenCellID, for: indexPath) as! FMOneKeyListenCell
cell.oneKeyListenList = viewModel.oneKeyListenList
return cell
}else if moduleType == "live" {
let cell:HomeRecommendLiveCell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeRecommendLiveCellID, for: indexPath) as! HomeRecommendLiveCell
cell.liveList = viewModel.liveList
return cell
}
else {
let cell:FMRecommendForYouCell = collectionView.dequeueReusableCell(withReuseIdentifier: FMRecommendForYouCellID, for: indexPath) as! FMRecommendForYouCell
return cell
}
}
複製程式碼
專案中分割槽尺寸高度是根據返回資料的Count
進行計算的,其他各模組基本思路相同這裡只貼一下首頁模組分割槽的尺寸高度計算。
// item 尺寸
func sizeForItemAt(indexPath: IndexPath) -> CGSize {
let HeaderAndFooterHeight:Int = 90
let itemNums = (self.homeRecommendList?[indexPath.section].list?.count)!/3
let count = self.homeRecommendList?[indexPath.section].list?.count
let moduleType = self.homeRecommendList?[indexPath.section].moduleType
if moduleType == "focus" {
return CGSize.init(width:YYScreenWidth,height:360)
}else if moduleType == "square" || moduleType == "topBuzz" {
return CGSize.zero
}else if moduleType == "guessYouLike" || moduleType == "paidCategory" || moduleType == "categoriesForLong" || moduleType == "cityCategory" || moduleType == "live"{
return CGSize.init(width:YYScreenWidth,height:CGFloat(HeaderAndFooterHeight+180*itemNums))
}else if moduleType == "categoriesForShort" || moduleType == "playlist" || moduleType == "categoriesForExplore"{
return CGSize.init(width:YYScreenWidth,height:CGFloat(HeaderAndFooterHeight+120*count!))
}else if moduleType == "ad" {
return CGSize.init(width:YYScreenWidth,height:240)
}else if moduleType == "oneKeyListen" {
return CGSize.init(width:YYScreenWidth,height:180)
}else {
return .zero
}
}
複製程式碼
首頁分類模組分析:
首頁分類採用的是CollectionView
展示分類列表,點選每個分類Item
進入對應的分類介面,根據categoryId
請求頂部滾動title
資料,另外該資料不包含推薦模組,所以分類整體為兩個Controller
,一個為推薦模組,一個為其他分類介面根據不同categoryId
顯示不同資料列表(因為該介面資料樣式一樣都是列表),然後推薦部分按照首頁的同等思路根據不同的moduleType
顯示不同型別Cell
。
首頁Vip模組分析:
首頁Vip
模組與推薦模組較為相似,頂部Banner
滾動圖片和分類按鈕作為頂部Cell
,然後其他Cell
橫向顯示或者是豎向顯示以及顯示的Item
數量根據介面而定,分割槽的標題同樣來自於介面資料,點選分割槽headerVeiw
的更多按鈕跳轉到該分割槽模組的更多頁面。
首頁直播模組分析:
首頁直播介面的排版主要分四個部分也就是自定義四個CollectionCell
,頂部分類按鈕,接著是Banner
滾動圖片Cell
內部使用FSPagerView
實現滾動圖片效果,滾動排行榜為Cell
內部巢狀CollectionView
,通過定時器控制CollectionCell
實現自動滾動,接下來就是播放列表了,通過自定義HeaderView
上面的按鈕切換,重新整理不同型別的播放列表。
首頁廣播模組分析:
首頁廣播模組主要分三個部分,頂部分類按鈕Cell
,中間可展開收起分類Item
,因為介面中返回的是14
個電臺分類,收起狀態顯示7
個電臺和展開按鈕,展開狀態顯示14
個電臺和收起按鈕中間空一格Item
,在ViewModel
中獲取到資料之後進行插入圖片按鈕並根據當前展開或是收起狀態返回不同Item
資料來實現這部分功能,剩下的是根據資料介面中的分割槽顯示列表和HeaderView
內容。
點選廣播頂部分類Item
跳轉到對應介面,但是介面返回的該Item
引數為Url
中拼接的欄位例如:url:"iting://open?msg_type=70&api=http://live.ximalaya.com/live-web/v2/radio/national&title=國家臺&type=national",所以我們要解析Url
拼接引數為字典,拿到我們所需的跳轉下一介面請求介面用到的欄位。下面為程式碼部分:
func getUrlAPI(url:String) -> String {
// 判斷是否有引數
if !url.contains("?") {
return ""
}
var params = [String: Any]()
// 擷取引數
let split = url.split(separator: "?")
let string = split[1]
// 判斷引數是單個引數還是多個引數
if string.contains("&") {
// 多個引數,分割引數
let urlComponents = string.split(separator: "&")
// 遍歷引數
for keyValuePair in urlComponents {
// 生成Key/Value
let pairComponents = keyValuePair.split(separator: "=")
let key:String = String(pairComponents[0])
let value:String = String(pairComponents[1])
params[key] = value
}
} else {
// 單個引數
let pairComponents = string.split(separator: "=")
// 判斷是否有值
if pairComponents.count == 1 {
return "nil"
}
let key:String = String(pairComponents[0])
let value:String = String(pairComponents[1])
params[key] = value as AnyObject
}
guard let api = params["api"] else{return ""}
return api as! String
}
複製程式碼
我聽模組分析:
我聽模組主頁面頂部為自定義HeaderView
,內部迴圈建立按鈕,下面為使用LTScrollView
管理三個子模組的滾動檢視,訂閱和推薦為固定列表顯示介面資料,一鍵聽模組也是現實列表資料,其中有個跑馬燈滾動顯示重要內容的效果,點選新增頻道,跳轉更多頻道介面,該介面為雙TableView
實現聯動效果,點選左邊分類LeftTableView
對應右邊RightTableView
滾動到指定分割槽,滾動右邊RightTableView
對應的左邊LeftTableView
滾動到對應分類。
發現模組分析:
發現模組主頁面頂部為自定義HeaderView
,內部巢狀CollectionView
建立分類按鈕Item
,下面為使用LTScrollView
管理三個子模組的滾動檢視,關注和推薦動態類似都是顯示圖片加文字形式顯示動態,這裡需要注意的是根據文字內容和圖片的張數計算當前Cell
的高度,趣配音就是正常的列表顯示。
下面貼一個計算動態釋出距當前時間的程式碼
複製程式碼
//MARK: -根據後臺時間戳返回幾分鐘前,幾小時前,幾天前
func updateTimeToCurrennTime(timeStamp: Double) -> String {
//獲取當前的時間戳
let currentTime = Date().timeIntervalSince1970
//時間戳為毫秒級要 / 1000, 秒就不用除1000,引數帶沒帶000
let timeSta:TimeInterval = TimeInterval(timeStamp / 1000)
//時間差
let reduceTime : TimeInterval = currentTime - timeSta
//時間差小於60秒
if reduceTime < 60 {
return "剛剛"
}
//時間差大於一分鐘小於60分鐘內
let mins = Int(reduceTime / 60)
if mins < 60 {
return "\(mins)分鐘前"
}
//時間差大於一小時小於24小時內
let hours = Int(reduceTime / 3600)
if hours < 24 {
return "\(hours)小時前"
}
//時間差大於一天小於30天內
let days = Int(reduceTime / 3600 / 24)
if days < 30 {
return "\(days)天前"
}
//不滿足上述條件---或者是未來日期-----直接返回日期
let date = NSDate(timeIntervalSince1970: timeSta)
let dfmatter = DateFormatter()
//yyyy-MM-dd HH:mm:ss
dfmatter.dateFormat="yyyy年MM月dd日 HH:mm:ss"
return dfmatter.string(from: date as Date)
}
複製程式碼
我的模組分析:
我的介面在這裡被劃分為了三個模組,頂部的頭像、名稱、粉絲等一類個人資訊作為TableView
的HeaderView
,並且在該HeaderView
中迴圈建立了已購、優惠券等按鈕,然後是Section0
迴圈建立錄音、直播等按鈕,下面的Cell
根據dataSource
進行分割槽顯示及每個分割槽的count
。在我的介面中使用了兩個小動畫,一個是上下滾動的優惠券引導領取動畫,另一個是我要錄音一個波狀擴散提示錄音動畫。
下面貼一下波紋擴散動畫的程式碼
複製程式碼
import UIKit
class CVLayerView: UIView {
var pulseLayer : CAShapeLayer! //定義圖層
override init(frame: CGRect) {
super.init(frame: frame)
let width = self.bounds.size.width
// 動畫圖層
pulseLayer = CAShapeLayer()
pulseLayer.bounds = CGRect(x: 0, y: 0, width: width, height: width)
pulseLayer.position = CGPoint(x: width/2, y: width/2)
pulseLayer.backgroundColor = UIColor.clear.cgColor
// 用BezierPath畫一個原型
pulseLayer.path = UIBezierPath(ovalIn: pulseLayer.bounds).cgPath
// 脈衝效果的顏色 (註釋*1)
pulseLayer.fillColor = UIColor.init(r: 213, g: 54, b: 13).cgColor
pulseLayer.opacity = 0.0
// 關鍵程式碼
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.bounds = CGRect(x: 0, y: 0, width: width, height: width)
replicatorLayer.position = CGPoint(x: width/2, y: width/2)
replicatorLayer.instanceCount = 3 // 三個複製圖層
replicatorLayer.instanceDelay = 1 // 頻率
replicatorLayer.addSublayer(pulseLayer)
self.layer.addSublayer(replicatorLayer)
self.layer.insertSublayer(replicatorLayer, at: 0)
}
func starAnimation() {
// 透明
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0 // 起始值
opacityAnimation.toValue = 0 // 結束值
// 擴散動畫
let scaleAnimation = CABasicAnimation(keyPath: "transform")
let t = CATransform3DIdentity
scaleAnimation.fromValue = NSValue(caTransform3D: CATransform3DScale(t, 0.0, 0.0, 0.0))
scaleAnimation.toValue = NSValue(caTransform3D: CATransform3DScale(t, 1.0, 1.0, 0.0))
// 給CAShapeLayer新增組合動畫
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [opacityAnimation,scaleAnimation]
groupAnimation.duration = 3 //持續時間
groupAnimation.autoreverses = false //迴圈效果
groupAnimation.repeatCount = HUGE
groupAnimation.isRemovedOnCompletion = false
pulseLayer.add(groupAnimation, forKey: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
複製程式碼
播放模組分析:
播放模組可以說是整個專案主線的終點,前面模組點選跳轉進入具體節目介面,主頁面頂部為自定義HeaderView
,主要顯示該有聲讀物的一些介紹,背景為毛玻璃虛化,下面為使用LTScrollView
管理三個子模組的滾動檢視,簡介為對讀物和作者的介紹,節目列表為該讀物分章節顯示,找相似為與此相似的讀物,圈子為讀者分享圈幾個子模組都是簡單的列表顯示,子模組非固定是根據介面返回資料決定有哪些子模組。
點選節目列表任一Cell
就跳轉到播放詳情介面,該介面採用分割槽CollectionCell
,頂部Cell
為整體的音訊播放及控制,因為要實時播放音訊所以沒有使用AVFoudtion
,該框架需要先快取本地在進行播放,而是使用的三方開源的Streaming
庫來線上播放音訊,剩下的為作者發言和評論等。
總結:
目前專案中主要模組的介面和功能基本完成,寫法也都是比較簡單的寫法,專案用時很短,目前一些功能模組使用了第三方。接下來
1、準備替換為自己封裝的控制元件
2、把專案中可以複用的部分抽離出來封裝為靈活多用的公共元件
3、對當前模組進行一些Bug
修改和當前功能完善。
在這件事情完成之後準備對整體程式碼進行Review
,之後進行接下來功能模組的仿寫。
最後:
感興趣的朋友可以到GitHub
:github.com/daomoer/XML…
下載原始碼看看,也請多提意見,喜歡的朋友動動小手給點個Star
✨✨