一 部分功能图(后面会完善)
二 讲解思路
1 项目目录结构搭建
2 抽取工具类
3 自定义cell
4 分层思想
5 业务逻辑
三 项目目录搭建和相关配置
1 采用搭建搭建结构思路 : MCV模式
—-> 1.1 文件夹图片 :
2 注意 一 : 当我们在创建目录的时候,直接将info.plist文件拖入到System的时候,编译的时候,会报错.原因是找不到info.plist文件.
—-> 2.1 处理方式 : 直接找到工程文件,然后找到General,里面会有一个现实info的按钮,点击即可解决.
3 注意二 : 修改好了注意一的点,然后编译的时候会出现警告,警告提示info.plist文件已经加载过了,这是找到对应的info.plist文件,按照下面配置就可解决.
4 设置应用图标和应用启动图片我这里就不再讲了,前面博客中都有说明
5 分析采用纯代码搭建还是采用storyboard搭建项目结构?
—-> 5.1 通过app的部分效果图可以知道,采用storyboard是最简单的选择(UINavigationController;UITableViewController;UIViewController)
6 由于需要点击某行的cell然后跳转页面,这里我们直接绑定一个segue
四 创建模型
1 设置展示在tableView中数据的模型
2 根据提供数据的plist文件,设置需要写在模型中的属性
—-> 2.1 经验 : 按照下面的方式操作可以提高开发效率(一些开发细节)
—-> 2.2 图一 :通过下面的方式打开plist文件
—-> 2.3 图二 : 赋值打开后的plist文件中一对字典数据
—-> 2.4 直接在模型的文件中通过替换的方式直接对2.3的数据操作,可以很快的设置需要的属性.
3 模型属性代码
/** 歌曲名称 */
var name: String?
/** 歌曲文件名称 */
var filename: String?
/** 歌曲文件名称 */
var lrcname: String?
/** 演唱者 */
var singer: String?
/** 歌手头像 */
var singerIcon: String?
/** 专辑图片 */
var icon: String?
4 用KVC的方式来实现转模型
//重写了下面的方法,此方法是必须重写的
override init() {
super.init()
}
/** KVC */
init(dict : [String : AnyObject]) {
super.init()
setValuesForKeysWithDictionary(dict)
}
5 容错处理
—-> 5.1 原因 : 如果不重写下面的代码,当字典中的key和value有对不上号的情况下,会直接报错,所以这里我们做出容错处理.
//异常处理
override func setValue(value: AnyObject?, forUndefinedKey key: String) {
}
五 封装工具类
1 封装原因 : 提供接口,代码复用
2 返回模式 : 采用闭包回调的方式将数据回调出去
3 最终目的 : 此方法的设计,只需要在外界调用的时候,负责提供音乐中的数据,具体怎么提供,就是工具类中的问题了.
4 采用闭包回调的好处 : 如果以后想讲请求的方式更换为网络请求,那么如果采用闭包回调的方式就不需要修改接口,直接将接口内部的实现修改成网络请求就可以达到目的.(这里是直接加载本地的数据)
5 工具类文件夹
6 代码部分 :
—-> 6.1 判断
—-> 6.2 遍历
class XFJQQMusicDataTool: NSObject {
//** 设计一个方法,用来返回数据模型 */
class func getMusicList( result: (musicMs : [XFJQQMusicModel]) ->()) {
//加载本地的plist文件
guard let path = NSBundle.mainBundle().pathForResource("Musics.plist", ofType: nil) else {
result(musicMs: [XFJQQMusicModel]())
return
}
//加载文件的内容
guard let dictArray = NSArray(contentsOfFile: path) else {
result(musicMs: [XFJQQMusicModel]())
return
}
//把字典转成模型
var resultMs = [XFJQQMusicModel]()
//遍历
for dict in dictArray {
let musicM = XFJQQMusicModel(dict: dict as! [String : AnyObject])
resultMs.append(musicM)
}
//以闭包的形式返回出去
result(musicMs: resultMs)
}
}
六 UITableViewController的相关设置
1 展示数据的tableView文件(注意和storyboard中的控制器绑定)
2 设置tableView的背景图片
3 设置状态栏的样式
4 设置导航条隐藏
5 上面都是对tableView的处理方式,我们直接采用类扩展,然后将三个方法放在一个供外界提供的方法中,让外界通过直接调用一个方法,就能对三个要求的设置
6 代码 :
extension XFJQQListTVC {
//初始化设置
func setUpInit() {
setUpTableView()
setUpNavigationBar()
}
//通过一个方法来设置tableView的相关设置
private func setUpTableView() {
//tableView的背景图片
let backView = UIImageView(image: UIImage(named: "QQListBack"))
tableView.backgroundView = backView
//cell的高度
tableView.rowHeight = 60
//分割线
tableView.separatorStyle = .None
}
//设置导航条隐藏
private func setUpNavigationBar() {
navigationController?.navigationBarHidden = true
}
//设置状态栏的样式
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
}
7 在viewDidLoad中直接调用setUpInit()方法和对工具类提供的方法调用
override func viewDidLoad() {
super.viewDidLoad()
XFJQQMusicDataTool.getMusicList { (musicMs) -> () in
//将返回的数组装入模型中
self.musicMs = musicMs
}
setUpInit()
}
8 在展示内容的tableView中提供一个展示数据的模型的set方法
—-> 8.1 目的 : 当数据只要发生改变,就刷新tableView
//只要有数据变化,就刷新表格
var musicMs : [XFJQQMusicModel] = [XFJQQMusicModel]() {
didSet {
tableView.reloadData()
}
}
七 自定义cell
1 自定义原因 : 系统的cell无法满足需求
2 采用方式 : xib(由于每行cell内容的样式都一样)
3 创建继承与UITableViewCell的文件,同时创建xib
4 xib的相关设置
—-> 4.1 我这里只说明绑定cell的ID,以便重复利用(其它的不说了)
5 通过拖线的方式,拿到xib中的属性
//** 歌手的头像 */
@IBOutlet weak var iconImageView: UIImageView!
//** 歌名 */
@IBOutlet weak var songName: UILabel!
//** 演唱者 */
@IBOutlet weak var singerName: UILabel!
6 重写模型的set方法,以便外界通过set方法来赋值(这里我直接给上代码,没什么好说明的,注意容错处理就行)
//重写模型的set方法,用模型给xib的属性赋值
var musicM : XFJQQMusicModel? {
didSet {
//容错处理
if musicM?.icon != nil {
iconImageView.image = UIImage(named:musicM!.icon!)
}
songName.text = musicM?.name
singerName.text = musicM?.singer
}
}
7 有app的效果图我们需要对明星的头像处理
//当加载xib的时候一定会调用
override func awakeFromNib() {
super.awakeFromNib()
//处理头像
iconImageView.layer.cornerRadius = iconImageView.width * 0.5
iconImageView.layer.masksToBounds = true
}
8 在cell的内部提供一个方法,让外界直接调用就可以拿到cell,不需要将该方法写在创建cell的数据源方法中,只要负责提供一个接口就行,至于怎么实现的,就交给cell内部,外界不需要管(自定义cell的核心部分,需要大家学习学习,并且掌握).
//提供一个方法,返回由xib创建的cell
class func cellWithTableView(tableView : UITableView) ->XFJQQMusicListCell {
let cellID = "cellID"
var cell = tableView.dequeueReusableCellWithIdentifier(cellID) as? XFJQQMusicListCell
if cell == nil {
//检查是否有循环利用
print("创建cell")
cell = NSBundle.mainBundle().loadNibNamed("XFJQQMusicListCell", owner: nil, options: nil).first as? XFJQQMusicListCell
}
return cell!
}
9 取消点击cell的高亮状态(也是属于cell的,直接写在cell的内部就可以)
//重写cell的hightlight方法,一遍点击cell的时候cell不在高亮
override func setHighlighted(highlighted: Bool, animated: Bool) {
}
10 cell的动画效果(核心动画)
—-> 10.1 该部分也是属于cell内部的事情,直接将代码写在自定义cell内部就可以
—-> 10.2 怎么样构建接口?通过由外界传入的枚举值,用switch来作为判断以哪种模式来执行cell的动画
—-> 10.3 在cell的内部定义枚举值
//动画的样式
enum AnimationType {
case Rotation
case Translation
}
—-> 10.4 cell的动画
///MARK : - 动画(核心动画)
extension XFJQQMusicListCell {
func beginAnimation(type : AnimationType) {
//用switch做判断(执行哪种动画)
switch type {
case .Rotation:
//移除上一个动画
self.layer.removeAnimationForKey("rotation")
//做怎样的动画
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
//动画之间的角度
animation.values = [M_PI * 0.25, 0]
//动画时长
animation.duration = 0.2
//添加动画
self.layer.addAnimation(animation, forKey: "rotation")
case .Translation:
self.layer.removeAnimationForKey("translation")
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.values = [60, 0]
animation.duration = 0.3
self.layer.addAnimation(animation, forKey: "translation")
}
}
}
11 数据源方法
—-> 11.1 tableView的行数(由模型的个数决定)
//** 行 */
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return musicMs.count
}
—-> 11.2 每个cell显示的内容(获取自定义cell,内容有模型决定)
//** cell显示的内容 */
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//通过方法获得cell
let cell = XFJQQMusicListCell.cellWithTableView(tableView)
//取出模型
let musicM = musicMs[indexPath.row]
//赋值(cell.musicM: 模型的set方法,通过该方法来赋值)
cell.musicM = musicM
//返回cell
return cell
}
—-> 11.3 开始做动画(调用cell中动画的API,注意将cell的类型转为自定义的类型)
//开始做动画
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
//将系统的cell转为xib的cell
let cell = cell as! XFJQQMusicListCell
//开始动画
cell.beginAnimation(AnimationType.Translation)
}
八 封装对音乐操作的工具类(分三层调用的思想)–>第一层
1 封装工具类的原因 : 对外界提供一个接口,在内部只负责对音乐的播放,暂停,快进和快退等功能.外面不需要关心内部怎么实现的,值负责调用接口就可以
2 创建工具类文件
3 播放音乐文件
—-> 3.1 设置接口 : 需要让外界传入一个音乐文件名,然后在方法内做出一系列的判断,才开始播放音乐文件
class XFJQQMusicTool: NSObject {
//创建播放器
var avplayer : AVAudioPlayer?
//设置一个方法,又外界直接传入一个文件名,然后就直接播放
func getplayMusic(name : String) ->(){
//获取传入的文件url(并且判断)
guard let url = NSBundle.mainBundle().URLForResource(name, withExtension: nil) else {return}
//由url来判断播放的是否是同一首歌
if url == avplayer?.url {
//播放的是同一首歌
avplayer?.play()
return
}
//播放传入的url的歌曲
do {
avplayer = try AVAudioPlayer(contentsOfURL: url)
}catch{
//打印错误信息
print(error)
return
}
//准备播放
avplayer?.prepareToPlay()
//开始播放
avplayer?.play()
}
4 提供两个接口作为控制播放器中间按钮控制暂停和重新播放的状态(内部直接控制,外界只需要调用接口)
//** 重新播放 */
func resumeCurrentMusic() ->() {
avplayer?.play()
}
//** 暂停播放 */
func pauseCurrentMusic() ->() {
avplayer?.pause()
}
九 音乐的业务逻辑工具类的封装(分层思想第二层)
1 封装原因 : 通过该曾来调用最内部的第一层来实现相应的业务逻辑
2 封装第二层工具类的文件
3 设置单例 : 保证每次创建的都是同一个对象
//设置单例
static let shareInstance = XFJQQMusicOperationTool()
4 用设置属性记录当前播放音乐的角标的方式来实现点击上一首和下一首音乐的播放(提供一个set方法)
//设置一个属性记录正在播放的音乐的角标(并且提供set方法)
var index = 0 {
didSet {
if index < 0
{
index = (musicList?.count ?? 0) - 1
}
if index > (musicList?.count ?? 0) - 1
{
index = 0
}
}
}
5 上一首歌曲(内部直接调用播放音乐的方法)–>因为每次点击上一首歌曲的时候,都是从头开始播放的
//** 上一首 */
func preMusic() ->() {
index -= 1
//判断
if let tempList = musicList {
//取出模型
let musicM = tempList[index]
//根据模型播放音乐
getplayMusic(musicM)
}
}
6 下一首歌曲播放(内部直接调用播放音乐的方法)–> 因为每次点击下一首歌曲的时候,都是从头开始播放的
//** 下一首 */
func nextMusic() ->() {
index += 1
//判断
if let tempList = musicList {
//取出模型
let musicM = tempList[index]
//根据模型播放音乐
getplayMusic(musicM)
}
}
7 播放音乐(直接在该类中创建一个方法来调用第一层中播放音乐的方法.注意需要闯入音乐的文件名)
//创建模型的音乐播放列表
var musicList : [XFJQQMusicModel]?
//创建操作音乐的对象
let tool = XFJQQMusicTool()
//提供一个方法由外界通过这个方法播放音乐
func getplayMusic(musicM : XFJQQMusicModel) ->() {
//通过传入的模型,拿到模型中歌名属性(filename)
let fileName = musicM.filename ?? ""
//完成对歌曲的播放
tool.getplayMusic(fileName)
//容错处理(如果没有歌曲,就直接退出)
if musicList == nil {
return
}
//记录下当前在播放的音乐的索引
index = (musicList?.indexOf(musicM))!
}
8 播放和暂停(通过调用第一层中的方法来实现)
//** 播放 */
func playCurrentMusic() ->() {
tool.resumeCurrentMusic()
}
//** 暂停 */
func pauseCurrentMusic() ->() {
tool.pauseCurrentMusic()
}
9 在点击cell的数据源方法中,直接调用第二层中的接口,就能达到播放音乐的目的(第三层调用)
//用户点击了某行cell,播放对应的歌曲
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//取出行模型
let musicM = musicMs[indexPath.row]
//播放
XFJQQMusicOperationTool.shareInstance.getplayMusic(musicM)
//通过绑定的suge,当点击某行cell的时候,跳转到下一个界面
performSegueWithIdentifier("listToDetail", sender: nil)
}
十 展示歌词
1 分析 : 通过app的功能可以看出来,拖动显示歌词的view可以往左滑动,那么显示歌词的view可以暂时用普通的view来做,装view的父控件就是UIScrollerView,因为需要滚动的.
2 创建显示暂停,开始,上一首和下一首的类(详情页)
3 布局详情页(这里就不细说了,但是要注意毛玻璃的设置可以通过代码,也可以通过直接在storyboard中拖一个UIToolBar控件来设置)
4 通过从storyboard中的拖线拿到相应的控件
—-> 4.1 下面这个类只负责属性(这样分层也是一种方便管理的思想)
///MARK : - 只负责属性
class XFJQQDetailVC: UIViewController {
//** 歌手头像 */
@IBOutlet weak var foreImageView: UIImageView!
//** 歌词能显示的并且滚动的View */
@IBOutlet weak var lrcBackView: UIScrollView!
//** 进度 */
@IBOutlet weak var progressSlider: UISlider!
//** 歌名 */
@IBOutlet weak var lrcLabel: UILabel!
//** 展示歌词的view */
var lrcview : UIView?
}
5 拖过拖线处理暂停,开始,上一首和下一首的业务逻辑
—-> 5.1 扩充一个类扩展,将方法写在里面(注意里面的方法是通过拿到单例来调用的)
//** 下一首音乐 */
@IBAction func nextButton()
{
XFJQQMusicOperationTool.shareInstance.nextMusic()
}
//** 上一首音乐 */
@IBAction func previous()
{
XFJQQMusicOperationTool.shareInstance.preMusic()
}
//** 播放和暂停 */
@IBAction func playAndPause(button : UIButton)
{
button.selected = !button.selected
//判断
if button.selected {
XFJQQMusicOperationTool.shareInstance.pauseCurrentMusic()
}else {
XFJQQMusicOperationTool.shareInstance.playCurrentMusic()
}
}
6 在上面的类扩展中我们通过设置两个方法来调用下面部分的代码
//** 设置方法用来添加调用一次性的设置 */
private func setUpOnce() ->() {
addLrcView()
setProgressSlider()
}
//** 设置方法用来添加调用多次的设置 */
private func setUpLrcFrame() ->() {
setUpLrcViewFrame()
setUpForeImage()
}
7 设置进度条的背景图片(只需要设置一次)
//** 设置进度条的背景图片 */
private func setProgressSlider() ->() {
progressSlider.setThumbImage(UIImage(named: "player_slider_playback_thumb"), forState: UIControlState.Normal)
}
8 计算显示歌词的view的frame(执行多次)
//** 计算显示歌词的view的frame */
private func setUpLrcViewFrame() {
//lrcview的frame值
lrcview?.frame = lrcBackView.bounds
//lrcview的x值
lrcview?.x = lrcBackView.width
//设置拖动显示歌词的view的contensize
lrcBackView.contentSize = CGSizeMake(lrcBackView.width * 2.0, 0)
}
9 负责添加显示歌词的控件view(只需要添加一次即可)
//** 负责添加控件(只需要添加一次即可) */
private func addLrcView() ->() {
//创建显示歌词的view
lrcview = UIView()
//设置背景颜色
lrcview?.backgroundColor = UIColor.redColor()
//添加到滚动的view中
lrcBackView.addSubview(lrcview!)
//开启分页模式
lrcBackView.pagingEnabled = true
//隐藏水平滚动条
lrcBackView.showsHorizontalScrollIndicator = false
//设置scorllView的代理
lrcBackView.delegate = self
}
10 设置图片的圆角和状态栏的样式
//** 设置圆角图片 */
private func setUpForeImage() ->() {
foreImageView.layer.cornerRadius = foreImageView.width * 0.5
foreImageView.layer.masksToBounds = true
}
//设置状态栏的样式
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
11 在第6点中我们直接通过两个方法来调用同时也达到了分层的调用
//这些方法只需要调用一次即可
override func viewDidLoad() {
super.viewDidLoad()
setUpOnce()
}
// 在视图加载好之后, 有可能, 里面拿到的不是真实的最终frame, 有可能是xib里面的大小
//该方法会频繁调用,但是调用越频繁,frame越准确
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
setUpLrcFrame()
}
12 当拖动显示歌词的view的时候,背景图片慢慢的呈现透明样式,可直接通过下面代理方法解决
///MARK : - scrollView的代理方法的实现
extension XFJQQDetailVC : UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
let alpha = 1 - scrollView.contentOffset.x / scrollView.width
//设置背景图片的透明度
foreImageView.alpha = alpha
//设置显示歌名的label透明度
lrcLabel.alpha = alpha
}
}
十一 总结
1 可能出现错误的地方 : 在处理详情页中间的图片圆角可能会出现图片不是很圆的情况?
2 直接在viewWillLayoutSubviews中设置才是最准确的(解决).
3 掌握工具类的分层思想对初学者来说可能有点不能接受,但是能明白将是一种非常实用的思想(理解).
4 注意自定义cell中,在自定义cell的内部提供一个放发,直接返回cell,不需要将cell的实现写在数据源方法中,只需要在数据源方法中调用接口,就能达到目的(掌握).
5 最后,希望大家尽可能的去理解吧,不懂的可以随时给我留言.如果大家觉得我写的博客还满意的话,麻烦大家关注我的官方博客,谢谢!!!!
时间: 2024-10-05 07:04:35