使用SnapKit约束label、scrollview和tableview

本文介绍了如何使用SnapKit库在Swift中约束UILabel、UIScrollView和UITableViewCell。强调了在设置UILabel时不应设置固定高度,而在处理UIScrollView时,通过在其中添加viewContainer来实现contentSize自适应。对于UITableViewCell,可以将其视为viewContainer,简化高度计算。文章提供关键代码示例。

Snapkit:https://github.com/SnapKit/SnapKit

一、约束UILabel

这个其实本来是很简单的,被自己搞的复杂了,当设置UILabel的时候frame:CGRect.zero就可以了,另外行数设置为:0,一定一定不要设置固定高度 /(ㄒoㄒ)/~~,frame里不要,约束里也不要,可以设置大于等于。

let lb = UILabel(frame: CGRect.zero)
lb.font = UIFont.systemFont(ofSize: 15)
// 设置为0, 才可以自动换行
lb.numberOfLines = 0
lb.text = "这本应该是iOS中一个标准、内置的解决空table和collection view的方式。默认的如果你的table view是空的,屏幕就是空的。但这不是你能提供的最好的用户体验。\n 用了这个库,你只需要遵循一系列协议,iOS会优雅地接管你的collection view并且会正确、好看地显示给用户信息。很明显,每个iOS项目都应该采用。\n在使用第三方类库时,使用cocoaPods是非常方便的,具体使用方法可以参考:CocoaPods安装和使用教程 的安装使用方法。今天讨论的问题是,我在使用的时候遇到了一些问题"
superView.addSubview(lb)
// 一定要在加到父view后才可以用   
lb.snp.makeConstraints { (make) in
    make.top.equalToSuperview().offset(8)
    make.left.equalToSuperview().offset(8)
    make.right.equalToSuperview().offset(-8)
}

二、约束UIScrollView

由于UIScrollView比较特别,真正的高度是contentSize.height,其中的元素不能直接对UIScrollView约束。

解决的方法是,在其中加一个viewContainer (UIView),让scrollview的contentSize.height自适应viewContainer,而viewContainer的高度自适应与其中的各个控件。

重点内容都在代码里:

import UIKit
import SnapKit

class rootViewController: UIViewController {


    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.gray
        self.title = "自动布局"

        // 如果使用了UINavigationController,scroll的上方会出现一片空白区域。设为false可以取消
        // 只有当scroll为第一个控件的时候会出现
        self.automaticallyAdjustsScrollViewInsets = false


        let sc = UIScrollView(frame: CGRect.zero)
        self.view.addSubview(sc)

        sc.backgroundColor = UIColor.white
        sc.snp.makeConstraints { (make) in
            make.top.equalToSuperview().offset(120)
            make.left.equalToSuperview().offset(15)
            make.right.equalToSuperview().offset(-15)
            make.bottom.equalToSuperview().offset(-15)
        }

        let viewContainer = UIView(frame:CGRect.zero)
        sc.addSubview(viewContainer)

        viewContainer.backgroundColor = UIColor.green

        viewContainer.snp.makeConstraints { (make) in
            make.edges.width.equalTo(sc)
            make.top.equalTo(sc)
            // 这个很重要!!!!!!
            // 必须要比scroll的高度大一,这样才能在scroll没有填充满的时候,保持可以拖动
            make.height.greaterThanOrEqualTo(sc).offset(1)
        }

        let lb = UILabel(frame: CGRect.zero)
        lb.font = UIFont.systemFont(ofSize: 15)
        lb.numberOfLines = 0
        lb.textColor = UIColor.black
        lb.text = "这本应该是iOS中一个标准、内置的解决空table和collection view的方式。默认的如果你的table view是空的,屏幕就是空的。但这不是你能提供的最好的用户体验。\n 用了这个库,你只需要遵循一系列协议,iOS会优雅地接管你的collection view并且会正确、好看地显示给用户信息。很明显,每个iOS项目都应该采用。\n在使用第三方类库时,使用cocoaPods是非常方便的,具体使用方法可以参考:CocoaPods安装和使用教程 的安装使用方法。今天讨论的问题是,我在使用的时候遇到了一些问题"
        viewContainer.addSubview(lb)

        lb.snp.makeConstraints { (make) in
            make.top.equalToSuperview().offset(8)
            make.left.equalToSuperview().offset(8)
            make.right.equalToSuperview().offset(-8)
        }

        let btn1 = UIButton(type: UIButtonType.system)
        btn1.setTitle("显示更多", for: .normal)
        btn1.frame = CGRect.zero
        btn1.backgroundColor = UIColor.purple
        btn1.setTitleColor(UIColor.white, for: .normal)

        viewContainer.addSubview(btn1)

        btn1.snp.makeConstraints { (make) in
            make.top.equalTo(lb.snp.bottom).offset(5)
            make.width.equalTo(100)
            make.height.equalTo(30)
            // 这个很重要,viewContainer中的最后一个控件一定要约束到bottom,并且要小于等于viewContainer的bottom
            // 否则的话,上面的控件会被强制拉伸变形
            // 最后的-15是边距,这个可以随意设置
            make.bottom.lessThanOrEqualTo(viewContainer).offset(-15)
            make.centerX.equalToSuperview()
        }
    }

    func btnClick(sender:UIButton) {
        let detail = DetailViewController()
        self.navigationController?.pushViewController(detail, animated: true)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

三、约束UITableViewCell

基于上面的方法,这个就比较简单了,以前苦逼的计算每个cell的高度/(ㄒoㄒ)/~~

在这里可以直接把Cell当做上面的viewContainer就好了。重点是UITableView的属性设置:

tv = UITableView(frame: .zero, style: .plain)
// 可以自适应高度的重点就是这个
tv.estimatedRowHeight = 44
// 固定行高,我没看出这个的实际效果,谁能告知一下。。。\(^o^)/~
tv.rowHeight = UITableViewAutomaticDimension
self.view.addSubview(tv)
tv.snp.makeConstraints { (make) in
    make.left.right.bottom.equalTo(self.view)
    make.top.equalTo(self.view).offset(60)
}

内容大概就这么多吧,别急着赶路,把路填平了,才能跑起来。

import UIKit import SnapKit class DeviceListNewViewController: SurveillanceCommonTableController { // MARK: - UI Components private lazy var fixedTitleView: UIView = { let view = UIView() view.backgroundColor = .systemRed.withAlphaComponent(0.8) // 🔴 return view }() private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "设备列表" label.font = UIFont.systemFont(ofSize: 20, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() private lazy var tabScrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsHorizontalScrollIndicator = true scrollView.bounces = true scrollView.alwaysBounceHorizontal = true scrollView.backgroundColor = .systemGreen.withAlphaComponent(0.5) // 🟢 return scrollView }() private let tabButtonTitles = ["所有设备", "收藏页面", "站点选择"] private var selectedTabIndex = 0 private var tabButtons: [UIButton] = [] private var collectionView: UICollectionView! private lazy var stickyContainer: UIView = { let view = UIView() view.backgroundColor = .clear return view }() // MARK: - Constraint private var stickyTopConstraint: Constraint? private var hasSetupHeader = false private let tabScrollViewHeight: CGFloat = 60 // MARK: - 👇 把 addSubViews 放进 setup 阶段,确保约束前已有父视图 override func tpbSetupSubviews() { super.tpbSetupSubviews() // ✅ 第一步:构建所有 view 层级关系(全部提前 addSubview) view.addSubview(fixedTitleView) view.addSubview(stickyContainer) view.addSubview(tableView) // 确保 tableView 是最后加的?根据你的基类逻辑调整 stickyContainer.addSubview(tabScrollView) setupTitleView() createTabButtons() setupCollectionView() // 添加按钮到 tabScrollView for button in tabButtons { tabScrollView.addSubview(button) } updateTabSelection(index: selectedTabIndex) } private func setupTitleView() { fixedTitleView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.greaterThanOrEqualToSuperview().offset(20) make.trailing.lessThanOrEqualToSuperview().offset(-20) } } private func createTabButtons() { tabButtons = tabButtonTitles.enumerated().map { index, title in let btn = UIButton(type: .custom) btn.tag = index btn.setTitle(title, for: .normal) btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) btn.setTitleColor(.black, for: .normal) btn.setTitleColor(.systemBlue, for: .selected) btn.layer.borderWidth = 1 btn.layer.borderColor = UIColor(white: 0.85, alpha: 1.0).cgColor btn.layer.cornerRadius = 20 btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) btn.addTarget(self, action: #selector(onTabTapped(_:)), for: .touchUpInside) return btn } } private func setupCollectionView() { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical layout.minimumLineSpacing = 10 layout.minimumInteritemSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .gray collectionView.alwaysBounceVertical = true collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "DeviceCell") } // MARK: - ✅ 所有约束在此设置,此时所有 view 都已 addSubview override func tpbMakeConstraint() { // 固定标题栏 fixedTitleView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide) make.leading.trailing.equalToSuperview() make.height.equalTo(40) } // stickyContainer stickyContainer.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(tabScrollViewHeight) self.stickyTopConstraint = make.top.equalTo(fixedTitleView.snp.bottom).constraint } // ✅ 此时 tabScrollView 已经有父视图 stickyContainer → superview 不为 nil tabScrollView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) make.height.equalTo(tabScrollViewHeight - 8) } // tableView 占据剩余空间 tableView.snp.remakeConstraints { make in make.top.equalTo(stickyContainer.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } } // MARK: - Layout Buttons private func layoutTabButtonsInScrollView() { for button in tabButtons { button.snp.removeConstraints() } let buttonHeight: CGFloat = 36 let padding: CGFloat = 16 let spacing: CGFloat = 8 var lastButton: UIButton? for button in tabButtons { button.snp.makeConstraints { make in make.centerY.equalToSuperview() make.height.equalTo(buttonHeight) if let prev = lastButton { make.leading.equalTo(prev.snp.trailing).offset(spacing) } else { make.leading.equalTo(tabScrollView).offset(padding) } make.width.greaterThanOrEqualTo(80) make.width.lessThanOrEqualTo(120) } lastButton = button } if let last = lastButton { last.snp.makeConstraints { make in make.trailing.equalTo(tabScrollView).offset(-padding) } } tabScrollView.layoutIfNeeded() print("📊 tabScrollView contentSize: \(tabScrollView.contentSize)") } // MARK: - Data private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() let section = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() let numItems = 20 let itemsPerRow: CGFloat = 2 let rowHeight: CGFloat = 80 let lineSpacing: CGFloat = 10 let numRows = ceil(CGFloat(numItems) / itemsPerRow) let collectionHeight = numRows * rowHeight + (numRows - 1) * lineSpacing let collectionCellModel = TPBBaseTableCellModel.customContent(with: collectionView) collectionCellModel.height = TPBTableElementHeight.customHeight(collectionHeight) cellModels.append(collectionCellModel) section.cellModelArray = cellModels tempSectionArray.append(section) sectionArray = tempSectionArray tableView.reloadData() } override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self collectionView.dataSource = self collectionView.delegate = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard view.bounds.width > 0 else { return } if !hasSetupHeader { hasSetupHeader = true layoutTabButtonsInScrollView() reloadData() } } // MARK: - ScrollView Delegate override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) guard scrollView === tableView else { return } let offsetY = scrollView.contentOffset.y let fixedTitleBottom = fixedTitleView.frame.maxY if offsetY <= 0 { stickyTopConstraint?.update(offset: fixedTitleBottom) } else { let targetY = max(fixedTitleBottom, fixedTitleBottom + offsetY) stickyTopConstraint?.update(offset: targetY - offsetY) } view.layoutIfNeeded() } // MARK: - Actions @objc private func onBack() { navigationController?.popViewController(animated: true) } @objc private func onSettings() { print("Settings tapped") } @objc private func onTabTapped(_ sender: UIButton) { guard sender.tag != selectedTabIndex else { return } tabButtons[selectedTabIndex].isSelected = false selectedTabIndex = sender.tag sender.isSelected = true } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } } // MARK: - UICollectionViewDataSource extension DeviceListNewViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 20 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceCell", for: indexPath) cell.backgroundColor = .systemBlue return cell } } // MARK: - UICollectionViewDelegateFlowLayout extension DeviceListNewViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let padding: CGFloat = 32 + 10 let width = (collectionView.bounds.width > 0 ? collectionView.bounds.width : UIScreen.main.bounds.width) - padding return CGSize(width: width / 2, height: 80) } }
12-03
import UIKit import SnapKit class DeviceListNewViewController: SurveillanceCommonTableController { // MARK: - UI Components private lazy var fixedTitleView: UIView = { let view = UIView() view.backgroundColor = .systemRed.withAlphaComponent(0.8) // 🔴 return view }() private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "设备列表" label.font = UIFont.systemFont(ofSize: 20, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() private lazy var tabScrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsHorizontalScrollIndicator = true scrollView.bounces = true scrollView.alwaysBounceHorizontal = true scrollView.backgroundColor = .systemGreen.withAlphaComponent(0.5) // 🟢 return scrollView }() private let tabButtonTitles = ["所有设备", "收藏页面", "站点选择"] private var selectedTabIndex = 0 private var tabButtons: [UIButton] = [] private var collectionView: UICollectionView! private lazy var stickyContainer: UIView = { let view = UIView() view.backgroundColor = .clear return view }() // MARK: - Constraint private var stickyTopConstraint: Constraint? private var hasSetupHeader = false private let tabScrollViewHeight: CGFloat = 60 // MARK: - 👇 把 addSubViews 放进 setup 阶段,确保约束前已有父视图 override func tpbSetupSubviews() { super.tpbSetupSubviews() // ✅ 第一步:构建所有 view 层级关系(全部提前 addSubview) view.addSubview(fixedTitleView) view.addSubview(stickyContainer) view.addSubview(tableView) // 确保 tableView 是最后加的?根据你的基类逻辑调整 stickyContainer.addSubview(tabScrollView) setupTitleView() createTabButtons() setupCollectionView() // 添加按钮到 tabScrollView for button in tabButtons { tabScrollView.addSubview(button) } updateTabSelection(index: selectedTabIndex) } private func setupTitleView() { fixedTitleView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.greaterThanOrEqualToSuperview().offset(20) make.trailing.lessThanOrEqualToSuperview().offset(-20) } } private func createTabButtons() { tabButtons = tabButtonTitles.enumerated().map { index, title in let btn = UIButton(type: .custom) btn.tag = index btn.setTitle(title, for: .normal) btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) btn.setTitleColor(.black, for: .normal) btn.setTitleColor(.systemBlue, for: .selected) btn.layer.borderWidth = 1 btn.layer.borderColor = UIColor(white: 0.85, alpha: 1.0).cgColor btn.layer.cornerRadius = 20 btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) btn.addTarget(self, action: #selector(onTabTapped(_:)), for: .touchUpInside) return btn } } private func setupCollectionView() { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical layout.minimumLineSpacing = 10 layout.minimumInteritemSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .gray collectionView.alwaysBounceVertical = true collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "DeviceCell") } // MARK: - ✅ 所有约束在此设置,此时所有 view 都已 addSubview override func tpbMakeConstraint() { // 固定标题栏 fixedTitleView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide) make.leading.trailing.equalToSuperview() make.height.equalTo(40) } // stickyContainer stickyContainer.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.height.equalTo(tabScrollViewHeight) self.stickyTopConstraint = make.top.equalTo(fixedTitleView.snp.bottom).constraint } // ✅ 此时 tabScrollView 已经有父视图 stickyContainer → superview 不为 nil tabScrollView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) make.height.equalTo(tabScrollViewHeight - 8) } // tableView 占据剩余空间 tableView.snp.remakeConstraints { make in make.top.equalTo(stickyContainer.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } } // MARK: - Layout Buttons private func layoutTabButtonsInScrollView() { for button in tabButtons { button.snp.removeConstraints() } let buttonHeight: CGFloat = 36 let padding: CGFloat = 16 let spacing: CGFloat = 8 var lastButton: UIButton? for button in tabButtons { button.snp.makeConstraints { make in make.centerY.equalToSuperview() make.height.equalTo(buttonHeight) if let prev = lastButton { make.leading.equalTo(prev.snp.trailing).offset(spacing) } else { make.leading.equalTo(tabScrollView).offset(padding) } make.width.greaterThanOrEqualTo(80) make.width.lessThanOrEqualTo(120) } lastButton = button } if let last = lastButton { last.snp.makeConstraints { make in make.trailing.equalTo(tabScrollView).offset(-padding) } } tabScrollView.layoutIfNeeded() print("📊 tabScrollView contentSize: \(tabScrollView.contentSize)") } // MARK: - Data private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() let section = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() let numItems = 20 let itemsPerRow: CGFloat = 2 let rowHeight: CGFloat = 80 let lineSpacing: CGFloat = 10 let numRows = ceil(CGFloat(numItems) / itemsPerRow) let collectionHeight = numRows * rowHeight + (numRows - 1) * lineSpacing let collectionCellModel = TPBBaseTableCellModel.customContent(with: collectionView) collectionCellModel.height = TPBTableElementHeight.customHeight(collectionHeight) cellModels.append(collectionCellModel) section.cellModelArray = cellModels tempSectionArray.append(section) sectionArray = tempSectionArray tableView.reloadData() } override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self collectionView.dataSource = self collectionView.delegate = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard view.bounds.width > 0 else { return } if !hasSetupHeader { hasSetupHeader = true layoutTabButtonsInScrollView() reloadData() } } // MARK: - ScrollView Delegate override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) guard scrollView === tableView else { return } let offsetY = scrollView.contentOffset.y let fixedTitleBottom = fixedTitleView.frame.maxY if offsetY <= 0 { stickyTopConstraint?.update(offset: fixedTitleBottom) } else { let targetY = max(fixedTitleBottom, fixedTitleBottom + offsetY) stickyTopConstraint?.update(offset: targetY - offsetY) } view.layoutIfNeeded() } // MARK: - Actions @objc private func onBack() { navigationController?.popViewController(animated: true) } @objc private func onSettings() { print("Settings tapped") } @objc private func onTabTapped(_ sender: UIButton) { guard sender.tag != selectedTabIndex else { return } tabButtons[selectedTabIndex].isSelected = false selectedTabIndex = sender.tag sender.isSelected = true } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } } // MARK: - UICollectionViewDataSource extension DeviceListNewViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 20 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceCell", for: indexPath) cell.backgroundColor = .systemBlue return cell } } // MARK: - UICollectionViewDelegateFlowLayout extension DeviceListNewViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let padding: CGFloat = 32 + 10 let width = (collectionView.bounds.width > 0 ? collectionView.bounds.width : UIScreen.main.bounds.width) - padding return CGSize(width: width / 2, height: 80) } }
12-03
import UIKit import SnapKit // MARK: - DeviceListNewViewController class DeviceListNewViewController: SurveillanceCommonTableController { // MARK: - 子视图声明 private lazy var tabScrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = UIColor(white: 0.95, alpha: 1.0) scrollView.layer.cornerRadius = 8 scrollView.clipsToBounds = true return scrollView }() private let tabButtonTitles = ["所有设备", "收藏页面", "站点选择"] private var selectedTabIndex = 0 private var tabButtons: [UIButton] = [] // 吸顶头部 private lazy var stickyHeader: UIView = { let header = UIView() header.backgroundColor = tpbColorBackground() return header }() private lazy var backButton: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("←", for: .normal) btn.setTitleColor(UIColor.blue, for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(onBackTapped), for: .touchUpInside) return btn }() private lazy var locationButton: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("📍", for: .normal) btn.setTitleColor(UIColor.blue, for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(onLocationTapped), for: .touchUpInside) return btn }() private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "设备列表" label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.textColor = tpbColorTextPrimary() label.textAlignment = .center return label }() private lazy var searchButton: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("🔍", for: .normal) btn.setTitleColor(UIColor.blue, for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(onSearchTapped), for: .touchUpInside) return btn }() private lazy var separatorLine: UIView = { let line = UIView() line.backgroundColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 0.3) return line }() // 横向分页容器 private lazy var deviceCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: UIScreen.main.bounds.width - 32, height: 400) layout.minimumLineSpacing = 16 layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.isPagingEnabled = true cv.showsHorizontalScrollIndicator = false cv.bounces = true cv.delegate = self cv.dataSource = self cv.register(DevicePageCollectionViewCell.self, forCellWithReuseIdentifier: "DevicePageCell") return cv }() // 页面控制器指示器 private lazy var pageControl: UIPageControl = { let pc = UIPageControl() pc.numberOfPages = 3 pc.currentPage = 0 pc.pageIndicatorTintColor = UIColor(white: 0.7, alpha: 1.0) pc.currentPageIndicatorTintColor = UIColor.blue pc.hidesForSinglePage = true return pc }() // MARK: - 生命周期 override func viewDidLoad() { super.viewDidLoad() self.tableView.contentInsetAdjustmentBehavior = .never reloadData() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: false) } override func tpbSetupSubviews() { super.tpbSetupSubviews() // 添加吸顶栏 view.addSubview(stickyHeader) stickyHeader.addSubview(backButton) stickyHeader.addSubview(locationButton) stickyHeader.addSubview(titleLabel) stickyHeader.addSubview(searchButton) stickyHeader.addSubview(separatorLine) createTabButtons() layoutTabButtonsInScrollView() } override func tpbMakeConstraint() { stickyHeader.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide.snp.top) make.leading.trailing.equalToSuperview() make.height.equalTo(50) } backButton.snp.remakeConstraints { make in make.leading.equalTo(stickyHeader).offset(16) make.centerY.equalTo(stickyHeader) make.width.greaterThanOrEqualTo(30) } locationButton.snp.remakeConstraints { make in make.leading.equalTo(backButton.snp.trailing).offset(8) make.centerY.equalTo(backButton) make.size.equalTo(30) } titleLabel.snp.remakeConstraints { make in make.centerX.equalToSuperview() make.centerY.equalTo(backButton) } searchButton.snp.remakeConstraints { make in make.trailing.equalTo(stickyHeader).offset(-16) make.centerY.equalTo(backButton) make.width.greaterThanOrEqualTo(30) } separatorLine.snp.makeConstraints { make in make.height.equalTo(1 / UIScreen.main.scale) make.leading.trailing.bottom.equalToSuperview() } tableView.snp.remakeConstraints { make in make.top.equalTo(stickyHeader.snp.bottom).offset(10) make.leading.trailing.bottom.equalToSuperview() } } // MARK: - Tab 操作 private func createTabButtons() { tabButtons = tabButtonTitles.enumerated().map { index, title in let btn = UIButton(type: .custom) btn.tag = index btn.setTitle(title, for: .normal) btn.titleLabel?.font = UIFont.systemFont(ofSize: 14) btn.setTitleColor(UIColor(white: 0.4, alpha: 1.0), for: .normal) btn.setTitleColor(UIColor.blue, for: .selected) btn.layer.borderWidth = 1 btn.layer.borderColor = UIColor(white: 0.85, alpha: 1.0).cgColor btn.layer.cornerRadius = 20 btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) btn.addTarget(self, action: #selector(onTabTapped(_:)), for: .touchUpInside) return btn } updateTabSelection(index: selectedTabIndex) } @objc private func onTabTapped(_ sender: UIButton) { guard sender.tag != selectedTabIndex else { return } updateTabSelection(index: sender.tag) scrollToPage(index: sender.tag) } private func updateTabSelection(index: Int) { self.selectedTabIndex = index for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } pageControl.currentPage = index DispatchQueue.main.async { self.deviceCollectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true) } } private func layoutTabButtonsInScrollView() { tabScrollView.subviews.forEach { $0.removeFromSuperview() } var previousButton: UIButton? for button in tabButtons { tabScrollView.addSubview(button) button.snp.makeConstraints { make in make.centerY.equalToSuperview() make.height.equalTo(36) if let prev = previousButton { make.leading.equalTo(prev.snp.trailing).offset(8) } else { make.leading.equalTo(tabScrollView).offset(16) } make.width.greaterThanOrEqualTo(90) make.width.equalTo(button.titleLabel!.snp.width).offset(32).priority(.low) } previousButton = button } if let lastBtn = previousButton { lastBtn.snp.makeConstraints { make in make.trailing.equalTo(tabScrollView).offset(-16) } } tabScrollView.layoutIfNeeded() } // MARK: - 创建横向滑动设备区域(单个 cell 内容) private func createHorizontalScrollableDeviceView() -> UIView { let containerView = UIView() containerView.backgroundColor = .clear // Collection View deviceCollectionView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(deviceCollectionView) // Page Control pageControl.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(pageControl) NSLayoutConstraint.activate([ deviceCollectionView.topAnchor.constraint(equalTo: containerView.topAnchor), deviceCollectionView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), deviceCollectionView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), deviceCollectionView.height.constraint(equalToConstant: 400), pageControl.topAnchor.constraint(equalTo: deviceCollectionView.bottomAnchor, constant: 8), pageControl.centerX.constraint(equalTo: containerView.centerXAnchor), pageControl.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), ]) return containerView } // 主动滚动到某一页 private func scrollToPage(index: Int) { let indexPath = IndexPath(item: index, section: 0) deviceCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } // MARK: - 刷新设备列表(现在只刷新一次,作为单个 cell 插入) private func refreshDeviceListSection() { let cellModel = TPBBaseTableCellModel.customContent(with: createHorizontalScrollableDeviceView()) cellModel.height = TPBTableElementHeight.customHeight(440) // 400 + 8 + 32 let section = TPBTableSectionModel() section.cellModelArray = [cellModel] if sectionArray.count > 1 { sectionArray[1] = section } else { sectionArray.append(section) } tableView.reloadData() } // MARK: - 加载初始数据 private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() // Section 0: 卡片信息 let section0 = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() // 设备总数视图 let deviceCountView = createDeviceCountView() let deviceCountCellModel = TPBBaseTableCellModel.customContent(with: deviceCountView) deviceCountCellModel.height = TPBTableElementHeight.customHeight(60) cellModels.append(deviceCountCellModel) // 存储空间视图 let storageView = createStorageUsageView() let storageCellModel = TPBBaseTableCellModel.customContent(with: storageView) storageCellModel.height = TPBTableElementHeight.customHeight(60) cellModels.append(storageCellModel) // Tab 区域 let tabView = createTabContainerView() let tabCellModel = TPBBaseTableCellModel.customContent(with: tabView) tabCellModel.height = TPBTableElementHeight.customHeight(72) cellModels.append(tabCellModel) section0.cellModelArray = cellModels tempSectionArray.append(section0) // 初始化设备列表区域(单 cell 多页) refreshDeviceListSection() // 合并 sections sectionArray = tempSectionArray + Array(sectionArray.dropFirst(max(0, sectionArray.count - 1))) tableView.reloadData() } // MARK: - 自定义 View 构建函数 /// 获取背景色(兼容 iOS 12) private func tpbColorBackground() -> UIColor { if #available(iOS 13, *) { return UIColor.systemBackground } else { return UIColor(white: 1.0, alpha: 1.0) } } /// 获取主文本色(兼容 iOS 12) private func tpbColorTextPrimary() -> UIColor { if #available(iOS 13, *) { return UIColor.label } else { return UIColor.black } } private func createDeviceCountView() -> UIView { let view = UIView() view.backgroundColor = .clear let label = UILabel() label.text = "设备总数:32 台" label.font = UIFont.systemFont(ofSize: 16, weight: .regular) label.textColor = tpbColorTextPrimary() label.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) label.snp.makeConstraints { make in make.centerY.equalTo(view) make.leading.equalTo(view).offset(16) } return view } private func createStorageUsageView() -> UIView { let view = UIView() view.backgroundColor = .clear let label = UILabel() label.text = "已用存储:1.2 TB / 5 TB" label.font = UIFont.systemFont(ofSize: 16, weight: .regular) label.textColor = tpbColorTextPrimary() label.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) label.snp.makeConstraints { make in make.centerY.equalTo(view) make.leading.equalTo(view).offset(16) } return view } private func createTabContainerView() -> UIView { let container = UIView() container.backgroundColor = .clear tabScrollView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(tabScrollView) tabScrollView.snp.makeConstraints { make in make.leading.equalTo(container).offset(16) make.trailing.equalTo(container).offset(-16) make.centerY.equalTo(container) make.height.equalTo(44) } return container } // MARK: - Action 回调 @objc private func onBackTapped() { navigationController?.popViewController(animated: true) } @objc private func onLocationTapped() { print("定位按钮点击") } @objc private func onSearchTapped() { print("搜索按钮点击") } } // MARK: - UICollectionViewDataSource & Delegate extension DeviceListNewViewController: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 3 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DevicePageCell", for: indexPath) as! DevicePageCollectionViewCell switch indexPath.item { case 0: let devices = (0..<30).map { "所有设备 \($0 + 1)" } cell.configure(with: "所有设备", deviceNames: devices) case 1: let favorites = (0..<10).map { "收藏设备 \($0 + 1)" } cell.configure(with: "收藏页面", deviceNames: favorites) case 2: let sites = ["站点 A", "站点 B", "站点 C", "站点 D"] cell.configure(with: "站点选择", deviceNames: sites) default: break } return cell } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if scrollView === deviceCollectionView { let page = Int(scrollView.contentOffset.x / scrollView.bounds.width) updateTabSelection(index: page) } } } // MARK: - DevicePageCollectionViewCell(每一页的内容) class DevicePageCollectionViewCell: UICollectionViewCell { private let titleLabel = UILabel() private let innerStackView = UIStackView() private var deviceViews: [UIView] = [] override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { backgroundColor = UIColor(white: 0.95, alpha: 1.0) layer.cornerRadius = 12 clipsToBounds = true // 标题 titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .bold) titleLabel.textAlignment = .center titleLabel.textColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 1.0) // 内部堆叠视图 innerStackView.axis = .vertical innerStackView.spacing = 10 innerStackView.distribution = .fillProportionally // 布局 addSubview(titleLabel) addSubview(innerStackView) titleLabel.translatesAutoresizingMaskIntoConstraints = false innerStackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), innerStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), innerStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), innerStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), innerStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) ]) } func configure(with title: String, deviceNames: [String]) { titleLabel.text = title // 清除旧视图 for view in deviceViews { innerStackView.removeArrangedSubview(view) view.removeFromSuperview() } deviceViews.removeAll() // 添加新设备项 for name in deviceNames { let deviceView = createDeviceItemView(name: name) innerStackView.addArrangedSubview(deviceView) deviceViews.append(deviceView) } } private func createDeviceItemView(name: String) -> UIView { let view = UIView() view.backgroundColor = .white view.layer.cornerRadius = 8 view.layer.borderWidth = 1 view.layer.borderColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 0.3).cgColor let label = UILabel() label.text = name label.font = UIFont.systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 1.0) label.textAlignment = .center view.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.centerX.constraint(equalTo: view.centerXAnchor), label.centerY.constraint(equalTo: view.centerYAnchor) ]) return view } } // MARK: - DeviceListCell(保留原类用于其他场景或过渡) class DeviceListCell: UICollectionViewCell { private let titleLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupUI() } private func setupUI() { backgroundColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 0.1) layer.borderColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 1.0).cgColor layer.borderWidth = 1 layer.cornerRadius = 8 clipsToBounds = true titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium) titleLabel.textColor = UIColor(red: 0.0, green: 0.4, blue: 1.0, alpha: 1.0) titleLabel.textAlignment = .center titleLabel.numberOfLines = 1 titleLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.center.equalTo(contentView) make.leading.greaterThanOrEqualTo(contentView).offset(8) make.trailing.lessThanOrEqualTo(contentView).offset(-8) } } func configure(with title: String) { titleLabel.text = title } override func prepareForReuse() { super.prepareForReuse() titleLabel.text = nil } }这里面都使用snapkit布局,而且出现了错误:Value of type 'UICollectionView' has no member 'height'。func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if scrollView === deviceCollectionView { let page = Int(scrollView.contentOffset.x / scrollView.bounds.width) updateTabSelection(index: page) } } Overriding declaration requires an 'override' keyword
12-04
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值