- 最近抽空面了幾家公司,大部分都是從基礎開始慢慢深刻項目和原理。面試內容仍是以
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
進行開發。 項目中使用到的一些開源庫如下列表,在這裏感謝做者的開源。git
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
看起來更整潔一點,避免滿眼的邏輯判斷。github
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
,經過定時器控制器自動上下滾動。 web
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
。 json
首頁Vip
模塊與推薦模塊較爲類似,頂部Banner
滾動圖片和分類按鈕做爲頂部Cell
,而後其餘Cell
橫向顯示或者是豎向顯示以及顯示的Item
數量根據接口而定,分區的標題一樣來自於接口數據,點擊分區headerVeiw
的更多按鈕跳轉到該分區模塊的更多頁面。 swift
首頁直播界面的排版主要分四個部分也就是自定義四個CollectionCell
,頂部分類按鈕,接着是Banner
滾動圖片Cell
內部使用FSPagerView
實現滾動圖片效果,滾動排行榜爲Cell
內部嵌套CollectionView
,經過定時器控制CollectionCell
實現自動滾動,接下來就是播放列表了,經過自定義HeaderView
上面的按鈕切換,刷新不一樣類型的播放列表。 設計模式
首頁廣播模塊主要分三個部分,頂部分類按鈕Cell
,中間可展開收起分類Item
,由於接口中返回的是14
個電臺分類,收起狀態顯示7
個電臺和展開按鈕,展開狀態顯示14
個電臺和收起按鈕中間空一格Item
,在ViewModel
中獲取到數據以後進行插入圖片按鈕並根據當前展開或是收起狀態返回不一樣Item
數據來實現這部分功能,剩下的是根據數據接口中的分區顯示列表和HeaderView
內容。api
點擊廣播頂部分類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
滾動到對應分類。 bash
發現模塊主頁面頂部爲自定義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
庫來在線播放音頻,剩下的爲做者發言和評論等。
目前項目中主要模塊的界面和功能基本完成,寫法也都是比較簡單的寫法,項目用時很短,目前一些功能模塊使用了第三方。接下來 一、準備替換爲本身封裝的控件 二、把項目中能夠複用的部分抽離出來封裝爲靈活多用的公共組件 三、對當前模塊進行一些Bug
修改和當前功能完善。 在這件事情完成以後準備對總體代碼進行Review
,以後進行接下來功能模塊的仿寫。
感興趣的朋友能夠到GitHub
:github.com/daomoer/XML…
下載源碼看看,也請多提意見,喜歡的朋友動動小手給點個Star
✨✨