- 最近抽空面了几家公司,大部分都是从基础开始慢慢深刻项目和原理。面试内容仍是以
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
✨✨