歌单详情完整效果
歌单列表分组头部效果
本节是在文章 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 = .colorBackgroundLight
contentView.backgroundColor = .colorBackgroundLight
container.tg_gravity = TGGravity.vert.center
container.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 TangramKit
class SongGroupHeaderView : BaseTableViewHeaderFooterView{
static let NAME = "SongGroupHeaderView"
var countView:UILabel!
/// 播放所有点击
var playAllClick:(()->Void)!
override func initViews() {
super.initViews()
backgroundColor = .colorBackgroundLight
contentView.backgroundColor = .colorBackgroundLight
container.tg_gravity = TGGravity.vert.center
container.tg_padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: PADDING_SMALL)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onPlayAllTouchEvent(_:)))
//将触摸事件添加到当前view
container.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 = .colorPrimary
leftContainer.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 = .colorOnSurface
container.addSubview(titleView)
//数量
countView = UILabel()
countView.tg_width.equal(.fill)
countView.tg_height.equal(.wrap)
countView.text = "0"
countView.textAlignment = .left
countView.font = UIFont.systemFont(ofSize: TEXT_MEDDLE)
countView.textColor = .black80
container.addSubview(countView)
//下载按钮
let downloadButton = ViewFactoryUtil.button(image:R.image.arrowCircleDown()!.withTintColor())
downloadButton.tintColor = .colorOnSurface
downloadButton.tg_width.equal(50)
downloadButton.tg_height.equal(50)
container.addSubview(downloadButton)
//多选按钮
let multiSelectButton = ViewFactoryUtil.button(image:R.image.moreVerticalDot()!.withTintColor())
multiSelectButton.tintColor = .colorOnSurface
multiSelectButton.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 UIKit
class 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 = .clear
contentView.backgroundColor = .clear
container = 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)
//注册section
tableView.register(SongGroupHeaderView.self, forHeaderFooterViewReuseIdentifier: SongGroupHeaderView.NAME)
tableView.bounces = false
}
}
2)获取data分组列表数据
定义分组列表数据模型SongGroupData
//
// SongGroupData.swift
// 音乐分组模型
//
// Created by jin on 2024/9/13.
//
import Foundation
class 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 in
self?.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 = r
datum.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! SongGroupData
return data.datum.count
}
/// 返回section view
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//取出组数据
let groupData = datum[section] as! SongGroupData
//获取header
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: SongGroupHeaderView.NAME) as! SongGroupHeaderView
header.bind(groupData)
header.playAllClick = {[weak self] in
let groupData = self?.datum[1] as! SongGroupData
self?.play(groupData.datum[0] as! Song)
}
return header
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let groupData = datum[indexPath.section] as! SongGroupData
let data = groupData.datum[indexPath.row]
let type = typeForItemAtData(data)
switch type {
case .sheet:
let cell = tableView.dequeueReusableCell(withIdentifier: SheetInfoCell.NAME, for: indexPath) as! SheetInfoCell
cell.bind(data as! Sheet)
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: Constant.CELL, for: indexPath) as! SongItemCell
cell.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
}
//其他组不显示section
return 0
}
}
至此,实现了歌单列表分组头部效果。