前言
一直想利用空余时间写个开源项目,兜兜转转许久,光说不练都是空把戏,咱撸起袖子就是干。说做咱就开始利用空闲时间骚动起来吧。本开源项目讲解了一些App常见功能界面的搭建以及实现思路,适合新手以及正在学习Swift的同胞们。
号外
:最近似乎把自己活成了一个网瘾少年般的模样,一到某个点就开始相约鸡场。着实尴尬!!!
目录
一、关于项目
二、效果预览
三、详细讲解
3.0 欢迎模块分析
3.1 首页模块分析
3.2 我听模块分析
3.3 播放模块分析
3.4 发现模块分析
3.5 我的模块分析
四、遇到的问题以及解决措施
五、总结
一、关于项目
开发环境:Xcode 9.4.1,语言:Swift4.1
代码下载:代码下载地址,欢迎点赞和反馈
开发备注:对此项目一些数据是使用抓包工具进行获取,而图片的获取也是通过itool下载而取之。对于Charles抓包工具如果有想了解和使用的话可以查看笔者之前的文章。
然而抓包工具并不是万能的,对于有些接口传参方式是加密的,比如登录,比如调用某个接口的前提是必须先登录,然后才能掉下一个接口。咱就绕路而行吧。但是庆幸的是虽然有些接口虽不知以何种方式进行加密的,但是至少能抓取到返回数据,这就足够了。我们只要拿到返回的数据就好了。至少这个对我们只是想拿个项目练练手的话。就已经可以算是很人性化了。
喜马拉雅FM(项目搭建及层次结构剖析).png
项目中的方法层次图.png
二、效果预览
首页.gif
我听.gif
发现.gif
我的.gif
三、详细讲解
3.0 欢迎模块分析(引导页和广告页)
当程序被打开时,在创建KeyWindow的RootViewController时判断是否是首次打开,这里的逻辑是如果用户是首次打开应用则显示引导页(引导图片资料找了老半天没有找着,就直接从网络上拉了几张图片进来),当点击引导页最后一页的立即体验直接进入TabBarController,不显示广告页(效果如下图)
引导页.gif
如果用户不是首次打开应用的话,则请求网络加载图片显示广告页,并且在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 登录模块分析
由于登录接口请求方式加密了,故我们采取别的方式进行模拟用户是否登录
如下图:
image.png
接下来打开mocky网站,将上图返回的数据复制然后请继续往下看
mocky.gif
最后就可以通过该mocky生成的接口进行网络请求模拟请求了,接下来后面别的接口如遇到加密情况也将采取此种方式进行处理。
紧接着对用户登录的一些信息进行保存,下面贴一段归档的代码
//用户模型数据import UIKitimport HandyJSONclass 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 UIKitimport Foundation var instance:LoginHelper? = nilclass 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 首页模块分析
首页的数据较复杂一些,字母里面嵌套数组,数组里面再嵌套字典,字典又有数组字典。同时返回来的数据也相对其他接口多。首先我们先看推荐模块的接口数据
图片.png
顶部的banner(FMHomeHeaderView继承于TYCyclePagerView),作为 tableView.tableHeaderView 作为处理,而(猜你喜欢、精品、懒人一键听)作为viewForHeaderInSection,其他部分根据判断而返回不同样式的Cell。
3.3 我听模块分析
我听模块顶部为自定义ListenHeaderView,下面为使用LTScrollView管理三个子模块的滚动视图,订阅接口由于涉及用户登录相关,则拿不到实时接口,故采用Mocky模拟网络请求。而一键听和推荐可以抓到其接口并直接可以请求到数据,故直接拿的原生接口进行数据请求,且推荐页面做了上拉刷新以及下拉加载更多。订阅中的Cell和推荐中的cell采取的公用方式,只是稍微有点不一样而已,像这样大致一样的就没有必要再写多写一个cell了。一键听模块其中有个跑马灯滚动显示的效果,点击添加频道,跳转更多频道界面,(在页面跳转时,则统一在BaseNavViewController里面添加了返回按钮,以及侧滑返回。)该界面为双TableView实现联动效果,点击左边分类LeftTableView对应右边RightTableView滚动到指定分区,滚动右边RightTableView对应的左边LeftTableView滚动到对应分类。
图片.png
3.4 播放模块分析(待完善)
3.5 发现模块分析
发现页面总成分成两大部分,其中共涉及6个接口(绿色和蓝色框框表示需要请求的接口)。故采取MVVM的方式实现,将接口请求全部放在ViewModel,然后再根据需求进行数据回调。
因为登录后的关注界面和推荐界面跟这个差不多,所以直接服用推荐里面的cell。
未登录的关注则采用DZNEmptyDataSet开源框架,该框架挺不错的,笔者一直在使用。该框架目前已有1万多颗小星星。
图片.png
这里重点在于 图片展示的数量计算,以及根据文字内容和图片的张数计算当前Cell的高度。
实现思路: 这里cell主要是通过xib画的,我们需要把collectView的宽度和高度进行拖线,然后通过图片数量拿到对应的宽高度。然后设置collectionView宽高度的constant。紧接着拿到bottomView的最大Y值, 并将高度保存到viewModel模型中
图片.png
计算图片的思路
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。
作者:flowerflower
链接:https://www.jianshu.com/p/c9258d9855a5
共同学习,写下你的评论
评论加载中...
作者其他优质文章