内容简介:该项目采用与项目首页推荐模块,根据接口请求数据进行处理,顶部的
- 最近抽空面了几家公司,大部分都是从基础开始慢慢深入项目和原理。面试内容还是以
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
:sparkles::sparkles:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python for Data Analysis
Wes McKinney / O'Reilly Media / 2012-11-1 / USD 39.99
Finding great data analysts is difficult. Despite the explosive growth of data in industries ranging from manufacturing and retail to high technology, finance, and healthcare, learning and accessing d......一起来看看 《Python for Data Analysis》 这本书的介绍吧!
XML、JSON 在线转换
在线XML、JSON转换工具
HEX HSV 转换工具
HEX HSV 互换工具