歌单详情完整效果
歌单列表分组头部效果
本节是在文章 IOS 25 实现歌单详情(UITableView)列表② 的基础上,实现歌单列表分组头部View。当歌单列表滑动头部View至顶部时,头部View不会因列表滑动而消失,会一直显示在顶部。故,使用UITableViewHeaderFooterView 来实现。
实现逻辑
将歌单详情分为两组,图1为1组,图2为1组,每组都包含头部View(UITableViewHeaderFooterView);图1不需要头部View,则设置头部View隐藏,图2头部View在滚动歌单列表到顶部时,头部View会固定在顶部不消失。
歌单列表头部View实现
实现流程:
1.创建UITableViewHeaderFooterView,及在使用UITableView的Controller控制器上注册;
2.获取data分组数据,并调用UITableView的reloadData(),将数据更新到列表;
3.将data的Item数据绑定UITableView的每一个Section。
1)创建和注册HeaderFooterView
从效果图上面可以看出,歌单列表头部View由一个水平方向布局包含UIImageView和UITableView来实现。
自定义SongGroupHeaderView,继承自BaseTableViewHeaderFooterView,设置TGLinearLayout为水平方向。
class SongGroupHeaderView : BaseTableViewHeaderFooterView{static let NAME = "SongGroupHeaderView"override func initViews() {super.initViews()backgroundColor = .colorBackgroundLightcontentView.backgroundColor = .colorBackgroundLightcontainer.tg_gravity = TGGravity.vert.centercontainer.tg_padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: PADDING_SMALL)}override func getContainerOrientation() -> TGOrientation {return .horz}}
添加图标、全部播放文本和歌曲数量等布局,添加头部点击事件,并绑定布局数据。
//
// SongGroupHeaderView.swift
// MyCloudMusic
//
// Created by jin on 2024/9/13.
//import UIKit
import TangramKitclass SongGroupHeaderView : BaseTableViewHeaderFooterView{static let NAME = "SongGroupHeaderView"var countView:UILabel!/// 播放所有点击var playAllClick:(()->Void)!override func initViews() {super.initViews()backgroundColor = .colorBackgroundLightcontentView.backgroundColor = .colorBackgroundLightcontainer.tg_gravity = TGGravity.vert.centercontainer.tg_padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: PADDING_SMALL)let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onPlayAllTouchEvent(_:)))//将触摸事件添加到当前viewcontainer.addGestureRecognizer(tapGestureRecognizer)//左侧容器let leftContainer = TGRelativeLayout()leftContainer.tg_width.equal(50)leftContainer.tg_height.equal(50)container.addSubview(leftContainer)//图标let iconView = UIImageView()iconView.tg_width.equal(30)iconView.tg_height.equal(30)iconView.tg_centerX.equal(0)iconView.tg_centerY.equal(0)iconView.image = R.image.playCircle()?.withTintColor()iconView.tintColor = .colorPrimaryleftContainer.addSubview(iconView)//播放全部提示文本let titleView = UILabel()titleView.tg_width.equal(.wrap)titleView.tg_height.equal(.wrap)titleView.text = R.string.localizable.playAll()titleView.font = UIFont.boldSystemFont(ofSize: TEXT_LARGE2)titleView.textColor = .colorOnSurfacecontainer.addSubview(titleView)//数量countView = UILabel()countView.tg_width.equal(.fill)countView.tg_height.equal(.wrap)countView.text = "0"countView.textAlignment = .leftcountView.font = UIFont.systemFont(ofSize: TEXT_MEDDLE)countView.textColor = .black80container.addSubview(countView)//下载按钮let downloadButton = ViewFactoryUtil.button(image:R.image.arrowCircleDown()!.withTintColor())downloadButton.tintColor = .colorOnSurfacedownloadButton.tg_width.equal(50)downloadButton.tg_height.equal(50)container.addSubview(downloadButton)//多选按钮let multiSelectButton = ViewFactoryUtil.button(image:R.image.moreVerticalDot()!.withTintColor())multiSelectButton.tintColor = .colorOnSurfacemultiSelectButton.tg_width.equal(50)multiSelectButton.tg_height.equal(50)container.addSubview(multiSelectButton)}override func getContainerOrientation() -> TGOrientation {return .horz}@objc func onPlayAllTouchEvent(_ recognizer:UITapGestureRecognizer) {playAllClick()}func bind(_ data:SongGroupData) {countView.text = "(\(data.datum.count))"}
}
BaseTableViewHeaderFooterView实现
//
// BaseTableViewHeaderFooterView.swift
// TableViewHeaderFooterView基类
//
// Created by jin on 2024/9/13.
//import TangramKit
import UIKitclass BaseTableViewHeaderFooterView: UITableViewHeaderFooterView {// 对于需要动态评估高度的UITableViewCell来说可以把布局视图暴露出来。用于高度评估和边界线处理。以及事件处理的设置。var container: TGBaseLayout!override init(reuseIdentifier: String?) {super.init(reuseIdentifier: reuseIdentifier)innerInit()}required init?(coder: NSCoder) {super.init(coder: coder)innerInit()}func innerInit() {initViews()initDatum()initListeners()}/// 找控件func initViews() {backgroundColor = .clearcontentView.backgroundColor = .clearcontainer = TGLinearLayout(getContainerOrientation())container.tg_width.equal(.fill)container.tg_height.equal(.wrap)contentView.addSubview(container)}/// 设置数据func initDatum() {}/// 设置监听器func initListeners() {}/// 获取根容器布局方向func getContainerOrientation() -> TGOrientation {return .vert}/// 使用MyLayout后,让item自动计算高度,要重写该方法override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {return container.systemLayoutSizeFitting(targetSize)}
}
在SheetDetailController控制器,注册SongGroupHeaderView
class SheetDetailController: BaseTitleController {override func initViews() {super.initViews()title = R.string.localizable.sheet()// 注册单曲tableView.register(SongItemCell.self, forCellReuseIdentifier: Constant.CELL)tableView.register(SheetInfoCell.self, forCellReuseIdentifier: SheetInfoCell.NAME)//注册sectiontableView.register(SongGroupHeaderView.self, forHeaderFooterViewReuseIdentifier: SongGroupHeaderView.NAME)tableView.bounces = false}
}
2)获取data分组列表数据
定义分组列表数据模型SongGroupData
//
// SongGroupData.swift
// 音乐分组模型
//
// Created by jin on 2024/9/13.
//import Foundationclass SongGroupData: BaseModel {var datum:Array<Any>!
}
请求歌单详情接口获取歌单详情里的歌曲列表数据,组装成分组数据,更新tableView.reloadData()
class SheetDetailController: BaseTitleController {override func initDatum() {super.initDatum()loadData()}func loadData() {DefaultRepository.shared.sheetDetail(id).subscribeSuccess { [weak self] data inself?.show(data.data!)}.disposed(by: rx.disposeBag)}func show(_ data: Sheet) {self.data = data//第一组var groupData = SongGroupData()groupData.datum = [data]datum.append(groupData)//第二组if let r = data.songs {if !r.isEmpty {//有音乐才设置//设置数据groupData = SongGroupData()groupData.datum = rdatum.append(groupData)superFooterContainer.backgroundColor = .colorLightWhite}}tableView.reloadData()}
}
3)Item数据绑定Section
SheetDetailController控制器扩展父类的实现UITableViewDataSource:
1)实现numberOfSections方法,返回分组数量,即有多少组;
2)重写numberOfRowsInSection方法,返回每个分组内的列表长度,即当前组有多少个;
3)实现viewForHeaderInSection方法,创建对应的HeaderView,并将分组数据绑定到HeaderView。
4)重写cellForRowAt方法,创建对应的Cell,并将分组数据内的列表的Item数据绑定到Cell。
extension SheetDetailController {/// 有多少组func numberOfSections(in tableView: UITableView) -> Int {return datum.count}/// 当前组有多少个override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {let data = datum[section] as! SongGroupDatareturn data.datum.count}/// 返回section viewfunc tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {//取出组数据let groupData = datum[section] as! SongGroupData//获取headerlet header = tableView.dequeueReusableHeaderFooterView(withIdentifier: SongGroupHeaderView.NAME) as! SongGroupHeaderViewheader.bind(groupData)header.playAllClick = {[weak self] inlet groupData = self?.datum[1] as! SongGroupDataself?.play(groupData.datum[0] as! Song)}return header}override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let groupData = datum[indexPath.section] as! SongGroupDatalet data = groupData.datum[indexPath.row]let type = typeForItemAtData(data)switch type {case .sheet:let cell = tableView.dequeueReusableCell(withIdentifier: SheetInfoCell.NAME, for: indexPath) as! SheetInfoCellcell.bind(data as! Sheet)return celldefault:let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! SongItemCellcell.bind(data as! Song)cell.indexView.text = "\(indexPath.row + 1)"return cell}}/// header高度func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {if section == 1 {return 50}//其他组不显示sectionreturn 0}
}
至此,实现了歌单列表分组头部效果。