前言
一直想利用空余时间写个开源项目,兜兜转转许久,光说不练都是空把戏,咱撸起袖子就是干。说做咱就开始利用空闲时间骚动起来吧。本开源项目讲解了一些App常见功能界面的搭建以及实现思路,适合新手以及正在学习Swift的同胞们。
号外
:最近似乎把自己活成了一个网瘾少年般的模样,一到某个点就开始相约鸡场。着实尴尬!!!
目录
一、关于项目
二、效果预览
三、详细讲解
3.0 欢迎模块分析
3.1 首页模块分析
3.2 我听模块分析
3.3 播放模块分析
3.4 发现模块分析
3.5 我的模块分析
四、遇到的问题以及解决措施
五、总结
一、关于项目
开发环境:Xcode 9.4.1,语言:Swift4.1
代码下载:代码下载地址,欢迎点赞和反馈
开发备注:对此项目一些数据是使用抓包工具进行获取,而图片的获取也是通过itool下载而取之。对于Charles抓包工具如果有想了解和使用的话可以查看笔者之前的文章。
然而抓包工具并不是万能的,对于有些接口传参方式是加密的,比如登录,比如调用某个接口的前提是必须先登录,然后才能掉下一个接口。咱就绕路而行吧。但是庆幸的是虽然有些接口虽不知以何种方式进行加密的,但是至少能抓取到返回数据,这就足够了。我们只要拿到返回的数据就好了。至少这个对我们只是想拿个项目练练手的话。就已经可以算是很人性化了。
二、效果预览
三、详细讲解
3.0 欢迎模块分析(引导页和广告页)
当程序被打开时,在创建KeyWindow的RootViewController时判断是否是首次打开,这里的逻辑是如果用户是首次打开应用则显示引导页(引导图片资料找了老半天没有找着,就直接从网络上拉了几张图片进来),当点击引导页最后一页的立即体验直接进入TabBarController,不显示广告页(效果如下图)
如果用户不是首次打开应用的话,则请求网络加载图片显示广告页,并且在3秒后自动进入TabBarController(效果如下图)
逻辑代码如下:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
//加载欢迎页面
initLinkPage()
window?.makeKeyAndVisible()
return true
}
func initLinkPage() {
// 用来判断是否是第一次加载
let isFristOpen = UserDefaults.standard.object(forKey: "isFristOpen")
if isFristOpen == nil {
let guideVC = FMGuideViewController()
guideVC.finishBtnClickCallBack = {[weak self] () -> Void in
self?.initRootViewController()
}
window?.rootViewController = guideVC
UserDefaults.standard.set("isFristOpen", forKey: "isFristOpen")
}else{
loadAdViewController()
}
}
func loadAdViewController(){
let adVC = FMAdViewController()
adVC.skipBtnClickCallBack = { () -> Void in
self.initRootViewController()
}
window?.rootViewController = adVC
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.initRootViewController()
}
}
func initRootViewController() {
window?.rootViewController = BaseTabBarViewController()
}
这块的逻辑代码总共都不超过50行,至于引导页和广告页的具体实现代码,就不详细讲述了,可下载源码进行查看
3.1 登录模块分析
由于登录接口请求方式加密了,故我们采取别的方式进行模拟用户是否登录
如下图:
接下来打开mocky网站,将上图返回的数据复制然后请继续往下看
最后就可以通过该mocky生成的接口进行网络请求模拟请求了,接下来后面别的接口如遇到加密情况也将采取此种方式进行处理。
紧接着对用户登录的一些信息进行保存,下面贴一段归档的代码
//用户模型数据
import UIKit
import HandyJSON
class UserInfoModel: NSObject,HandyJSON,NSCoding {
required override init() { }
var token: String? //户授权的唯一票据
var uid: String? //用户的uid
var ret: Int = 1 //返回的状态码 0: 表示成功
var isLogin: Bool{
if LoginHelper.sharedInstance.userInfo?.token != nil {
return true
}else {
return false
}
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(token, forKey: "token")
aCoder.encode(uid, forKey: "uid")
}
required init?(coder aDecoder: NSCoder) {
token = aDecoder.decodeObject(forKey: "token") as? String
uid = aDecoder.decodeObject(forKey: "uid") as? String
}
}
登录信息的数据保存以及退出登录的本地数据清理都交由LoginHelper来处理
import UIKit
import Foundation
var instance:LoginHelper? = nil
class LoginHelper: NSObject {
var userInfo:UserInfoModel? {
didSet{
guard userInfo != nil else {
return
}
}
}
static let sharedInstance: LoginHelper = {
instance = LoginHelper()
instance?.userInfo = UserInfoModel()
let saveModel = NSKeyedUnarchiver.unarchiveObject(withFile: UserDataFilePath)
print("path:\(UserDataFilePath)")
if (saveModel != nil) {
instance?.userInfo = (saveModel as! UserInfoModel)
}
return instance!
}()
//MARK:保存用户信息
func saveUserInfo(userInfo: UserInfoModel) {
NSKeyedArchiver.archiveRootObject(userInfo, toFile: UserDataFilePath)
}
//MARK:清除用户信息
func clearUserInfo() {
instance = nil
userInfo?.token = nil
let clearUserInfo:Bool = ((try? FileManager.default.removeItem(atPath: UserDataFilePath)) != nil)
clearUserInfo ? print("清除用户数据成功"):print("清除用户数据失败")
}
//MARK:登录成功
class func loginSuccessDataHandle(){
NotificationCenter.default.post(name: NSNotification.Name(kLoginSuccessNotification), object: nil)
}
//MARK:退出登录 数据清理
class func loginOutDataHandle() {
LoginHelper.sharedInstance.clearUserInfo()
NotificationCenter.default.post(name: NSNotification.Name(kLogOutNotification), object: nil)
}
}
布局:登录页面布局有点粗糙,直接采取的是Xib的布局方式。
登录成果之后保存用户数据,再者登录成功之后要告诉相关页面做相应的UI更新
NetworkTool.shareNetworkTool().request(methodType: .GET, baseUrl: MAIN_URL_MOCKY, urlString: kLoginUrl, parameters: [:]) { (result, error) in
guard let resultDic = result as? [String : AnyObject] else{
return
}
let infoModel:UserInfoModel = UserInfoModel.deserialize(from: resultDic)!
if infoModel.ret == 0 {
LoginHelper.sharedInstance.userInfo = infoModel
LoginHelper.sharedInstance.saveUserInfo(userInfo: infoModel)
LoginHelper.loginSuccessDataHandle()
self.dismiss(animated: true, completion: nil)
}
}
3.2 首页模块分析
首页的数据较复杂一些,字母里面嵌套数组,数组里面再嵌套字典,字典又有数组字典。同时返回来的数据也相对其他接口多。首先我们先看推荐模块的接口数据
顶部的banner(FMHomeHeaderView继承于TYCyclePagerView),作为 tableView.tableHeaderView 作为处理,而(猜你喜欢、精品、懒人一键听)作为viewForHeaderInSection,其他部分根据判断而返回不同样式的Cell。
3.3 我听模块分析
我听模块顶部为自定义ListenHeaderView,下面为使用LTScrollView管理三个子模块的滚动视图,订阅接口由于涉及用户登录相关,则拿不到实时接口,故采用Mocky模拟网络请求。而一键听和推荐可以抓到其接口并直接可以请求到数据,故直接拿的原生接口进行数据请求,且推荐页面做了上拉刷新以及下拉加载更多。订阅中的Cell和推荐中的cell采取的公用方式,只是稍微有点不一样而已,像这样大致一样的就没有必要再写多写一个cell了。一键听模块其中有个跑马灯滚动显示的效果,点击添加频道,跳转更多频道界面,(在页面跳转时,则统一在BaseNavViewController里面添加了返回按钮,以及侧滑返回。)该界面为双TableView实现联动效果,点击左边分类LeftTableView对应右边RightTableView滚动到指定分区,滚动右边RightTableView对应的左边LeftTableView滚动到对应分类。
3.4 播放模块分析(待完善)
3.5 发现模块分析
发现页面总成分成两大部分,其中共涉及6个接口(绿色和蓝色框框表示需要请求的接口)。故采取MVVM的方式实现,将接口请求全部放在ViewModel,然后再根据需求进行数据回调。
因为登录后的关注界面和推荐界面跟这个差不多,所以直接服用推荐里面的cell。
未登录的关注则采用DZNEmptyDataSet开源框架,该框架挺不错的,笔者一直在使用。该框架目前已有1万多颗小星星。
这里重点在于 图片展示的数量计算,以及根据文字内容和图片的张数计算当前Cell的高度。
实现思路: 这里cell主要是通过xib画的,我们需要把collectView的宽度和高度进行拖线,然后通过图片数量拿到对应的宽高度。然后设置collectionView宽高度的constant。紧接着拿到bottomView的最大Y值, 并将高度保存到viewModel模型中
计算图片的思路
private func calculatePicViewSize(count:Int) -> CGSize {
/**
图片显示分几种情况:
1.没有配图
2.4张配图
3.其他张配图 (count -1)/3 + 1 = rows
*/
//1.没有配图
if count == 0 {
collectionViewBottomConst.constant = 0
return CGSize(width: 0, height: 0)
}
collectionViewBottomConst.constant = 10
// 取出picView对应的layout
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
//图片的WH
let imageViewWH = (screenW - 2 * magin - 2 * iteMagin) / 3
layout.itemSize = CGSize(width: imageViewWH, height: imageViewWH)
//2. 4张配图
if count == 4 {
let picViewWH = imageViewWH * 2 + iteMagin + 1 //+1微调
return CGSize(width: picViewWH, height: picViewWH)
}
// 3.其他张配图 (count -1)/3 + 1 = rows
/** 例子: 5张配图 2行 row:(5-1)/3+1 = 2*/
// 4.1 计算行数
let rows = CGFloat( (count - 1 )/3 + 1)
// 4.2 计算高度
let picViewH = rows * imageViewWH + (rows - 1 ) * iteMagin
// 4.3 计算宽度
let picViewW = screenW - 2 * magin
return CGSize(width: picViewW, height: picViewH)
}
3.6 我的模块分析
根据用户是否登录而进行一些业务逻辑的处理以及显示, 其中只做了扫一扫,退出登录(需清除本地数据),就没做那些详细界面了。上面的cell用xib直接画的,下面的Cell根据dataArr进行分区显示及每个分区的count。
参考写法
//www.greatytc.com/p/879f58fe3542
喜马拉雅FM
https://juejin.im/post/5b97743df265da0af21351aa#heading-11
下厨房
//www.greatytc.com/p/a8f619a2c622
四、遇到的问题以及解决措施
问题一: 导入HandyJSON报错
原因:
"_swift_getFieldAt", referenced from:
这个问题在 HandyJSON 4.2.0 版本 + XCcode Version 9.4.1 (9F2000) + swift 4.1 里面存在,但降级到 HandyJSON 4.1.3 就没有问题了。
额外提醒:
HandyJSON 开源框架中readme 里有说明
4.2.0 版本只支持 swift 4.2, swift 4.1 就用 4.1.3 版本
解决措施:
此时我们只要在Podfile
注明使用哪个基本即可 。例如:pod 'HandyJSON', '~> 4.1.3 '
问题二: MAC使用Charles,手机设置代理后,网页无法打开
解决措施:
点击Install Charles Root Certifficate会进入钥匙串中,之后
设置完了之后就可以正常访问网络勒
问题三: import JXMarqueeView 报错
解决措施:
General->Deployment Target -> 9.0 即可
最开始我的这里是8.0, 改完之后编译一下即可
五、总结
目前项目中主要模块的界面和功能(一级界面)基本完成,使用XIB以及SnapKit开源框架(相对于OC中的Masonry)进行布局。接下来
1、缺失功能的完善
2、对当前模块进行一些Bug修改和调整
3、根据返回数据进行细节优化以及调整
4、少玩多学,年尾已快到来,需对年初计划进行归纳和总结