IOS 18 发现界面(UITableView)Banner轮播图实现

发现界面完整效果

本文实现Banner轮播图效果

文章基于IOS 17 基于UITabBarController实现首页TabBar继续实现发现界面

实现逻辑

从发现界面的效果图可以看出,发现界面是一个列表,列表包含了不同的Item,我们可以将 banner部分看成是列表的一个Item(Cell),列表使用UITableView来实现。

封装BaseLogicController

由于返现界面控制器DiscoveryController和多个界面控制器都继承自BaseLogicController,而且列表UITableView在多个控制器上都需要用到,故将列表使用UITableView统一编写在父类BaseLogicController上,方便统一管理和使用。

实现代码:

Swift 复制代码
    /// 初始化TableView,四边都在安全区内
    func initTableViewSafeArea() {
        //外面添加一层容器,是方便在真实内容控件前后添加内容
        initLinearLayoutSafeArea()
        
        //tableView
        createTableView()
        
        container.addSubview(tableView)
    }

    /// 创建TableView,不会添加到任何布局
    func createTableView() {
        tableView = ViewFactoryUtil.tableView()
        tableView.delegate = self
        tableView.dataSource = self
    }

由于UITableView在多个地方会使用到,故统一在ViewFactoryUtil类上使用静态方法创建tableView,方便管理和复用。

Swift 复制代码
    static func tableView() -> UITableView {
        let r = QMUITableView()
        r.backgroundColor = .clear
        
        //去掉没有数据cell的分割线
        r.tableFooterView = UIView()
        
        //去掉默认分割线
        r.separatorStyle = .none
        
        //修复默认分割线,向右偏移问题
        r.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        
        r.tg_width.equal(.fill)
        r.tg_height.equal(.fill)
        
        //设置所有cell的高度为高度自适应,如果cell高度是动态的请这么设置。 如果不同的cell有差异那么可以通过实现协议方法-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
        //如果您最低要支持到iOS7那么请您实现-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法来代替这个属性的设置。
        r.rowHeight = UITableView.automaticDimension
        
        r.estimatedRowHeight = UITableView.automaticDimension
        
        //不显示滚动条
        r.showsVerticalScrollIndicator = false
        
        r.allowsSelection = true
        
        //分割线颜色
        r.separatorColor = .colorDivider
        
        return r
    }

BaseLogicController完整代码:

Swift 复制代码
//
//  BaseLogicController.swift
//  MyCloudMusic
//
//  Created by jin on 2024/8/19.
//

import UIKit

//提供类似Android中更高层级布局框架
import TangramKit

class BaseLogicController: BaseCommonController {
    
    /// 根容器
    var rootContainer: TGBaseLayout!
    
    /// 头部容器
    var superHeaderContainer: TGBaseLayout!
    var superHeaderContentContainer: TGBaseLayout!
    
    /// 容器
    var container: TGBaseLayout!
    
    /// 底部容器
    var superFooterContainer: TGBaseLayout!
    var superFooterContentContainer: TGBaseLayout!
    
    /// TableView
    var tableView: UITableView!
    
    lazy var datum: [Any] = {
        var r :[Any] = []
        return r
    }()

    /// 初始化RelativeLayout容器,四边都在安全区内
    func initRelativeLayoutSafeArea() {
        initLinearLayout()
        
        //header
        initHeaderContainer()
        
        //中间内容容器
        container = TGRelativeLayout()
        container.tg_width.equal(.fill)
        container.tg_height.equal(.fill)
        container.backgroundColor = .clear
        rootContainer.addSubview(container)
        
        //footer
        initFooterContainer()
    }
    
    /// 初始化垂直方向LinearLayout容器,四边都在安全区内
    func initLinearLayoutSafeArea(){
        initLinearLayout()
        
        //header
        initHeaderContainer()
        
        //中间内容容器
        container = TGLinearLayout(.vert)
        container.tg_width.equal(.fill)
        container.tg_height.equal(.fill)
        container.backgroundColor = .clear
        rootContainer.addSubview(container)
        
        //footer
        initFooterContainer()
    }
    
    /// 初始化TableView,四边都在安全区内
    func initTableViewSafeArea() {
        //外面添加一层容器,是方便在真实内容控件前后添加内容
        initLinearLayoutSafeArea()
        
        //tableView
        createTableView()
        
        container.addSubview(tableView)
    }
    
    /// 创建TableView,不会添加到任何布局
    func createTableView() {
        tableView = ViewFactoryUtil.tableView()
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    /// 使用默认分割线
    func initDefaultTableViewDivider() {
        tableView.separatorStyle = .singleLine
    }
    
    /// 初始化垂直方向LinearLayout容器
    func initLinearLayout() {
        rootContainer = TGLinearLayout(.vert)
        rootContainer.tg_width.equal(.fill)
        rootContainer.tg_height.equal(.fill)
        rootContainer.backgroundColor = .clear
        view.addSubview(rootContainer)
    }
    
    /// 头部容器,安全区外,一般用来设置头部到安全区外背景颜色
    func initHeaderContainer() {
        superHeaderContainer = TGLinearLayout(.vert)
        superHeaderContainer.tg_width.equal(.fill)
        superHeaderContainer.tg_height.equal(.wrap)
        superHeaderContainer.backgroundColor = .clear
        
        //头部内容容器,安全区内
        superHeaderContentContainer = TGLinearLayout(.vert)
        superHeaderContentContainer.tg_height.equal(.wrap)
        superHeaderContentContainer.tg_top.equal(TGLayoutPos.tg_safeAreaMargin)
        superHeaderContentContainer.tg_leading.equal(TGLayoutPos.tg_safeAreaMargin)
        superHeaderContentContainer.tg_trailing.equal(TGLayoutPos.tg_safeAreaMargin)
        superHeaderContentContainer.backgroundColor = .clear
        
        superHeaderContainer.addSubview(superHeaderContentContainer)
        rootContainer.addSubview(superHeaderContainer)
    }

    func initFooterContainer() {
        superFooterContainer = TGLinearLayout(.vert)
        superFooterContainer.tg_width.equal(.fill)
        superFooterContainer.tg_height.equal(.wrap)
        superFooterContainer.backgroundColor = .clear
        
        //底部内容容器,安全区内
        superFooterContentContainer = TGLinearLayout(.vert)
        superFooterContentContainer.tg_height.equal(.wrap)
        superFooterContentContainer.tg_bottom.equal(TGLayoutPos.tg_safeAreaMargin)
        superFooterContentContainer.tg_leading.equal(TGLayoutPos.tg_safeAreaMargin)
        superFooterContentContainer.tg_trailing.equal(TGLayoutPos.tg_safeAreaMargin)
        superFooterContentContainer.backgroundColor = .clear
        
        superFooterContainer.addSubview(superFooterContentContainer)
        rootContainer.addSubview(superFooterContainer)
    }
    
    override func initViews() {
        super.initViews()
        setBackgroundColor(.colorBackground)
    }
}

//TableView数据源和代理
extension BaseLogicController:UITableViewDataSource,UITableViewDelegate{
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return datum.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

列表UITableView实现流程

在IOS上,要实现列表UITableView的显示效果,需要以下三个流程:

1.创建UITableView;

2.创建Cell,及在使用UITableView的Controller控制器上注册Cell;

3.获取data列表数据,并调用UITableView的reloadData(),将数据更新到列表;

4.将data的Item数据绑定UITableView的每一个Cell。

1)创建UITableView

UITableView的创建已经在父类BaseLogicController上统一实现。下面重写DiscoveryController的initViews()方法,调用父类BaseLogicController创建UITableView。

Swift 复制代码
    override func initViews() {
        super.initViews()
        setBackgroundColor(.colorBackgroundLight)
        
        //初始化TableView结构
        initTableViewSafeArea()
       
    }

2)创建和注册Cell

创建UITableViewCell,由于UITableViewCell在多处使用,故统一封装BaseTableViewCell来实现。默认定义个水平方向的TGLinearLayout来实现,方向也定义了方法getContainerOrientation(),子类可以重写该方法实现布局方向。为了让item自动计算高度,重写了方法systemLayoutSizeFitting(),本文使用的是纯代码和TangramKitUI框架开发,不了解的可以看前面文章:IOS 02 SnapKit 纯代码开发IOS 04 TangramKit 纯代码开发

BaseTableViewCell实现:
Swift 复制代码
//
//  BaseTableViewCell.swift
//  通用TableViewCell
//
//  Created by jin on 2024/8/27.
//

import UIKit

//提供类似Android中更高层级布局框架
import TangramKit

class BaseTableViewCell:UITableViewCell{
    
    //对于需要动态评估高度的UITableViewCell来说可以把布局视图暴露出来。用于高度评估和边界线处理。以及事件处理的设置。
    var container:TGBaseLayout!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        innerInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        innerInit()
    }
    
    func innerInit() {
        initViews()
        initDatum()
        initListeners()
    }
    
    /// 找控件
    func initViews() {
        //背景透明
        backgroundColor = .clear
        contentView.backgroundColor = .clear
        
        //去掉默认的选中颜色
        selectionStyle = .none
        
        //根容器
        container = TGLinearLayout(getContainerOrientation())
        container.tg_width.equal(.fill)
        container.tg_height.equal(.wrap)
        container.tg_space = PADDING_MEDDLE
        contentView.addSubview(container)
    }
    
    func initDatum() {
        
    }
    
    func initListeners() {
        
    }
    
    /// 获取根容器布局方向
    func getContainerOrientation() -> TGOrientation {
        return .horz
    }
    
    /// 使用TangramKit后,让item自动计算高度,要重写该方法
    /// - Parameters:
    ///   - targetSize: <#targetSize description#>
    ///   - horizontalFittingPriority: <#horizontalFittingPriority description#>
    ///   - verticalFittingPriority: <#verticalFittingPriority description#>
    /// - Returns: <#description#>
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        return self.container.systemLayoutSizeFitting(targetSize)
    }
}
轮播图Cell BannerCell实现

轮播图实现效果稍微复杂,我们先显示一个图片来代替,等UITableView的完整效果实现出来,我们再来实现Cell的轮播图效果,图片BannerCell实现:

Swift 复制代码
//
//  BannerCell.swift
//  轮播图cell
//
//  Created by jin on 2024/8/27.
//

import UIKit

//提供类似Android中更高层级布局框架
import TangramKit

class BannerCell:BaseTableViewCell{
    
    var bannerData:BannerData!
    
    var datum:[String] = []
    
    override func initViews() {
        super.initViews()
        
        //底部的距离,由下一个控件设置,除非不方便设置
        container.tg_padding = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16)
        
        //轮播图
        let imageView = UIImageView()
        imageView.tg_width.equal(.fill)
        imageView.tg_height.equal(UIScreen.main.bounds.width * 0.389)
        imageView.image = R.image.placeholder()
        
        container.addSubview(imageView)
    }
    
    /// 绑定数据
    /// - Parameter data: <#data description#>
    func bind(_ data:BannerData) {
        bannerData = data
        
        
    }
}
注册BannerCell

重写DiscoveryController的initViews()方法,注册cell。

Swift 复制代码
    override func initViews() {
        super.initViews()
        setBackgroundColor(.colorBackgroundLight)
        
        //初始化TableView结构
        initTableViewSafeArea()
        
        //注册cell
        tableView.register(BannerCell.self, forCellReuseIdentifier: Constant.CELL)
    }

3)获取data列表数据

定义Banner数据模型BannerData

Swift 复制代码
//
//  BannerData.swift
//  发现界面轮播图模型
//
//  Created by jin on 2024/8/27.
//

import Foundation

class BannerData{
    var data:Array<Ad>!
    
    init(data: Array<Ad>!) {
        self.data = data
    }
}

在DiscoveryController控制器中,重写initDatum()方法,通过接口获取后端Banner数据。获取完数据后,一定要调用tableView.reloadData(),列表才会真正加载数据。网络请求还不熟悉的可以看文章:IOS 14 封装网络请求框架

Swift 复制代码
    override func initDatum() {
        super.initDatum()
        loadData()
    }
    
    func loadData() {
        DefaultRepository.shared.bannerAds().subscribeSuccess { [weak self] data in
            //清除原来的数据
            self?.datum.removeAll()
            
            //添加轮播图
            self?.datum.append(BannerData(data:data.data!.data!))
            
            self?.tableView.reloadData()
        }.disposed(by: rx.disposeBag)
    }

由于父类BaseLogicController已统一实现TableView数据源和代理扩展,将数据绑定列表,子类DiscoveryController不需要再操作。

Swift 复制代码
//TableView数据源和代理
extension BaseLogicController:UITableViewDataSource,UITableViewDelegate{
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return datum.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

4)Item数据绑定Cell

DiscoveryController控制器重写父类的扩展 cellForRowAt方法,创建对应的Cell,并将Item数据绑定到Cell。

Swift 复制代码
extension DiscoveryController{
    
    // 返回当前位置cell
    /// - Parameters:
    ///   - tableView: <#tableView description#>
    ///   - indexPath: <#indexPath description#>
    /// - Returns: <#description#>
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let data = datum[indexPath.row]
        
        //获取当前Cell的类型
        let type = typeForItemAtData(data)
        
        switch(type){
       
        default:
            //banner
            
            //取出一个Cell
            let cell = tableView.dequeueReusableCell(withIdentifier:  Constant.CELL, for: indexPath) as! BannerCell
            
            //绑定数据
            cell.bind(data as! BannerData)
            
            return cell
        }
    }
}

此时已实现Banner图片效果:

Banner轮播图实现

Banner轮播图实现是使用的一个第三方框架YJBannerView,这里也可以替换成自己常用的三方框架。完整实现代码如下:

Swift 复制代码
//
//  BannerCell.swift
//  轮播图cell
//
//  Created by jin on 2024/8/27.
//

import UIKit

//提供类似Android中更高层级布局框架
import TangramKit

class BannerCell:BaseTableViewCell{
    
    var bannerView:YJBannerView!
    
    var bannerData:BannerData!
    
    var datum:[String] = []
    
    var bannerClick:((Ad)->Void)!
    
    override func initViews() {
        super.initViews()
        
        //底部的距离,由下一个控件设置,除非不方便设置
        container.tg_padding = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16)
        
        //轮播图
        bannerView = YJBannerView()
        bannerView.backgroundColor = .clear
        bannerView.dataSource = self
        bannerView.delegate = self
        
        bannerView.tg_width.equal(.fill)
        //SCREEN_WIDTH是QMUI提供的宏
        //直接在initViews里面这样获取self.contentView.frame.size.width是默认值
        //而不是应用了自动布局后的值
        bannerView.tg_height.equal(UIScreen.main.bounds.width * 0.389)
        
        bannerView.clipsToBounds = true
        bannerView.layer.cornerRadius = 10

        //设置如果找不到图片显示的图片
        bannerView.emptyImage=R.image.placeholder()

        //设置占位图
        bannerView.placeholderImage=R.image.placeholder()

        //设置轮播图内部显示图片的时候调用什么方法
        bannerView.bannerViewSelectorString="sd_setImageWithURL:placeholderImage:"

        //设置指示器默认颜色
        bannerView.pageControlNormalColor = .black80

        //高亮的颜色
        bannerView.pageControlHighlightColor = .colorPrimary
        
        container.addSubview(bannerView)
    }
    
    /// 绑定数据
    /// - Parameter data: <#data description#>
    func bind(_ data:BannerData) {
        bannerData = data
        
        //清除原来的数据
        datum.removeAll()

        //循环每一个广告
        //取出广告的地址
        //放到一个数组中
        for it in data.data {
            datum.append(ResourceUtil.resourceUri(it.icon))
        }
        
        //通知轮播图框架从新加载数据
        bannerView.reloadData()
    }
}

// MARK: - banner数据源和代理
extension BannerCell:YJBannerViewDataSource,YJBannerViewDelegate{
    
    /// 返回BannerView要显示的数据
    ///
    /// - Parameter bannerView: <#bannerView description#>
    /// - Returns: <#return value description#>
    func bannerViewImages(_ bannerView: YJBannerView!) -> [Any]! {
        return datum
    }
    
    /// 自定义Cell
    /// 复写该方法的目的是
    /// 设置图片的缩放模式
    ///
    /// - Parameters:
    ///   - bannerView: <#bannerView description#>
    ///   - customCell: <#customCell description#>
    ///   - index: <#index description#>
    /// - Returns: <#return value description#>
    func bannerView(_ bannerView: YJBannerView!, customCell: UICollectionViewCell!, index: Int) -> UICollectionViewCell! {
        //将cell类型转为YJBannerViewCell
        let cell = customCell as! YJBannerViewCell

        //设置图片的缩放模式为
        //从中心填充
        //多余的裁剪掉
        cell.showImageViewContentMode = .scaleAspectFill

        return cell
    }
    
    /// banner点击回调方法
    ///
    /// - Parameters:
    ///   - bannerView: <#bannerView description#>
    ///   - index: <#index description#>
    func bannerView(_ bannerView: YJBannerView!, didSelectItemAt index: Int) {
        //获取当前点击的广告对象
        let r = bannerData.data[index]
        bannerClick(r)
    }
}

banner轮播图效果实现完成:

本文主要学习UITableView的使用,后续内容会慢慢实现 发现页面的全部内容。

相关推荐
iFlyCai9 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤18 小时前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc5588866618 小时前
iOS 18.1,未公开的新功能
ios
CocoaKier19 小时前
苹果商店下载链接如何获取
ios·apple
zhlx28351 天前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN1 天前
网易博客旧文----编译用于IOS的zlib版本
ios
爱吃香菇的小白菜1 天前
H5跳转App 判断App是否安装
前端·ios
二流小码农2 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
hxx2212 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift
B.-2 天前
在 Flutter 应用中调用后端接口的方法
android·flutter·http·ios·https