UIView.clipsToBounds 让子 View 只显示落在父 View 的 Frame 部分

探讨如何使用clipToBounds属性控制子视图超出父视图部分的显示,并比较与maskToBounds的区别。

默认情况下,我们向一个 View 中添加一个子 View 时,子 View 的越界部分也会显示出来,当然超过屏幕部分是显示不出来,因为无法在你手上显示内容了。看下面的图:

Unmi clipseToBounds     Unmi clipseToBounds

上图中,橙色的 View 是蓝色 View 的子 View。看左图,由它们的 Frame 设置,橙色的 View 部分超出的蓝色 View 的区域,默认情况下,蓝色 View 超出部分也会显示出来,除非在屏幕之外的部分。

而我们有时候不想要这种行为,希望越界部分不显示出来,像右图所示那盘。比如我们可能期望橙色 View 的动画效果只在它的父 View,即蓝色 View 之内展现。

其实问题很简单,正如标题中所提示的那个 clipToBounds 属性,字面意思是裁减到边界,就是超过父 View 的部分应该被裁减掉,而不显示。

只要设置父 View,即上图中的蓝色的 View 的 clipToBounds 为 YES 即可,默认为 NO.

blueView.clipToBounds = YES;

同时还应该考虑 CALayer 的 maskToBounds 的功能是什么,类似于 clipToBounds 吗?

maskToBounds 是 CALayer 的属性,基于 View  的不少属性其实就是作用于 CALayer 的。立即试验了一下,设置

blueView.layer.maskToBounds = YES 与 blueView.clipToBounds = YES 的效果是一样的,随便设置哪个属性都可。而且设置它们任何一个都会影响到 View 的阴影效果,因为阴影是加在 View 的 Frame 之外的,所以也被裁减去了。

import UIKit class FlowingImageViewController: UIViewController { private let containerView: UIView = { let view = UIView() view.backgroundColor = UIColor.clear view.layer.borderWidth = 2.0 view.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor view.layer.cornerRadius = 10 view.clipsToBounds = true view.translatesAutoresizingMaskIntoConstraints = false return view }() private let imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private var animator: UIViewPropertyAnimator? private var imageSize: CGSize = .zero private var containerSize: CGSize = .zero override func viewDidLoad() { super.viewDidLoad() setupViews() setupImage() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startAnimation() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopAnimation() } private func setupViews() { view.backgroundColor = .black view.addSubview(containerView) containerView.addSubview(imageView) NSLayoutConstraint.activate([ containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), containerView.widthAnchor.constraint(equalToConstant: 300), containerView.heightAnchor.constraint(equalToConstant: 200) ]) } private func setupImage() { // 使用示例图片,你可以替换为你自己的图片 if let image = UIImage(named: "sampleImage") ?? UIImage(systemName: "photo") { imageView.image = image imageSize = image.size contain
09-18
import UIKit class FlowingImageViewController: UIViewController { private let containerView: UIView = { let view = UIView() view.backgroundColor = UIColor.clear view.layer.borderWidth = 2.0 view.layer.borderColor = UIColor.white.withAlphaComponent(0.5).cgColor view.layer.cornerRadius = 10 view.clipsToBounds = true view.translatesAutoresizingMaskIntoConstraints = false return view }() private let imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private var animator: UIViewPropertyAnimator? private var imageSize: CGSize = .zero private var containerSize: CGSize = .zero override func viewDidLoad() { super.viewDidLoad() setupViews() setupImage() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startAnimation() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopAnimation() } private func setupViews() { view.backgroundColor = .black view.addSubview(containerView) containerView.addSubview(imageView) NSLayoutConstraint.activate([ containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), containerView.widthAnchor.constraint(equalToConstant: 300), containerView.heightAnchor.constraint(equalToConstant: 200) ]) } private func setupImage() { // 使用示例图片,你可以替换为你自己的图片 if let image = UIImage(named: "sampleImage") ?? UIImage(systemName: "photo") { imageView.image = image imageSize = image.size containerSize = CGSize(width: 300, height: 200) // 初始位置 - 居中 let initialX = (containerSize.width - imageSize.width) / 2 let initialY = (containerSize.height - imageSize.height) / 2 imageView.frame = CGRect(x: initialX, y: initialY, width: imageSize.width, height: imageSize.height) } } private func startAnimation() { stopAnimation() // 确保没有正在运行的动画 // 计算动画路径 let horizontalRange = imageSize.width - containerSize.width let verticalRange = imageSize.height - containerSize.height // 创建动画 animator = UIViewPropertyAnimator(duration: 10.0, curve: .easeInOut) { // 水平移动 if horizontalRange > 0 { let randomX = CGFloat.random(in: -horizontalRange...0) self.imageView.frame.origin.x = randomX } // 垂直移动 if verticalRange > 0 { let randomY = CGFloat.random(in: -verticalRange...0) self.imageView.frame.origin.y = randomY } } animator?.addCompletion { _ in // 动画完成后重新开始 self.startAnimation() } animator?.startAnimation() } private func stopAnimation() { animator?.stopAnimation(true) animator = nil } // 添加手势可以暂停/继续动画 @objc private func toggleAnimation() { if let animator = animator { if animator.isRunning { animator.pauseAnimation() } else { animator.startAnimation() } } else { startAnimation() } } }
09-18
根据示例代码修改我的代码,输出完整代码 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 = .tpbBackground return header }() private lazy var backButton: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("←", for: .normal) btn.setTitleColor(.systemBlue, 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(.systemBlue, 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 = .tpbTextPrimary label.textAlignment = .center return label }() private lazy var searchButton: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("🔍", for: .normal) btn.setTitleColor(.systemBlue, for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(onSearchTapped), for: .touchUpInside) return btn }() // 分隔线(用于 stickyHeader 底部) private lazy var separatorLine: UIView = { let line = UIView() line.backgroundColor = UIColor.separator.withAlphaComponent(0.3) return line }() // 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 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 紧贴 stickyHeader 下方 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(.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 } updateTabSelection(index: selectedTabIndex) } @objc private func onTabTapped(_ sender: UIButton) { guard sender.tag != selectedTabIndex else { return } tabButtons[selectedTabIndex].isSelected = false selectedTabIndex = sender.tag sender.isSelected = true // 可在此处刷新下方设备数据 refreshDeviceList() } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } // MARK: - 构建独立的 View 组件 /// 构建“设备数量”View private func createDeviceCountView() -> UIView { let view = UIView() view.backgroundColor = UIColor.yellow.withAlphaComponent(0.2) view.layer.cornerRadius = 8 view.clipsToBounds = true let label = UILabel() label.text = "设备总数:32 台" label.font = UIFont.systemFont(ofSize: 16, weight: .medium) label.textColor = .black label.textAlignment = .center view.addSubview(label) label.snp.makeConstraints { make in make.edges.equalTo(view).inset(12) } return view } /// 构建“存储空间”View private func createStorageUsageView() -> UIView { let view = UIView() view.backgroundColor = UIColor.orange.withAlphaComponent(0.2) view.layer.cornerRadius = 8 view.clipsToBounds = true let label = UILabel() label.text = "已用存储:1.2 TB / 5 TB" label.font = UIFont.systemFont(ofSize: 16, weight: .medium) label.textColor = .blue label.textAlignment = .center view.addSubview(label) label.snp.makeConstraints { make in make.edges.equalTo(view).inset(12) } return view } /// 构建 tabScrollView 的容器 View private func createTabScrollContainerView() -> UIView { let container = UIView() container.backgroundColor = .clear container.addSubview(tabScrollView) tabScrollView.snp.makeConstraints { make in make.edges.equalTo(container).inset(8) make.height.equalTo(40) } return container } // MARK: - 刷新设备列表(作为 cell 插入 table) private func refreshDeviceList() { // 示例:根据同 tab 显示同数据 let count = selectedTabIndex == 0 ? 30 : (selectedTabIndex == 1 ? 10 : 20) var cellModels = [TPBBaseTableCellModel]() for i in 0..<count { // 使用静态工厂方法创建 model let cellModel = TPBBaseTableCellModel.customContent { [weak self] in let cell = DeviceListCell(frame: .zero) cell.configure(with: "设备 \(i + 1)") return cell.contentView // 注意:返回的是 UIView是 cell 本身 } // 明确指定枚举类型 cellModel.height = TPBTableContentHeight.customHeight(80) cellModels.append(cellModel) } // 更新 section 数据 let section = TPBTableSectionModel() section.cellModelArray = cellModels // 假设你有一个 sectionArray 存储所有 section if self.sectionArray.count > 1 { self.sectionArray[1] = section // 第二个 section 是设备列表 } else { self.sectionArray.append(section) } tableView.reloadData() } // MARK: - 加载初始数据 private func reloadData() { var sectionArray = [TPBTableSectionModel]() let section = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() // 设备数量 Cell let deviceCountCellModel = TPBBaseTableCellModel.customContent(with: createDeviceCountView()) deviceCountCellModel.height = .customHeight(60) cellModels.append(deviceCountCellModel) // 存储空间 Cell let storageCellModel = TPBBaseTableCellModel.customContent(with: createStorageUsageView()) storageCellModel.height = .customHeight(60) cellModels.append(storageCellModel) // tabScrollView 区域 let tabScrollCellModel = TPBBaseTableCellModel.customContent(with: createTabScrollContainerView()) tabScrollCellModel.height = .customHeight(72) cellModels.append(tabScrollCellModel) section.cellModelArray = cellModels sectionArray.append(section) // 初始化设备列表(默认第一个 tab) refreshDeviceList() // 合并两个 section:上部分静态 + 下部分动态设备列表 self.sectionArray = sectionArray + self.sectionArray tableView.reloadData() } // MARK: - Action 回调 @objc private func onBackTapped() { print("返回") navigationController?.popViewController(animated: true) } @objc private func onLocationTapped() { print("定位") } @objc private func onSearchTapped() { print("搜索") } // MARK: - 布局 Tab 按钮到 ScrollView 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: - 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.systemBlue.withAlphaComponent(0.1) layer.borderColor = UIColor.systemBlue.cgColor layer.borderWidth = 1 layer.cornerRadius = 8 clipsToBounds = true titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium) titleLabel.textColor = .systemBlue titleLabel.textAlignment = .center titleLabel.numberOfLines = 1 contentView.addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.trailing.greaterThanOrEqualToSuperview().inset(8) } } func configure(with title: String) { titleLabel.text = title } override func prepareForReuse() { super.prepareForReuse() titleLabel.text = nil } }
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 }() // 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() { // super.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 } tabButtons[selectedTabIndex].isSelected = false selectedTabIndex = sender.tag sender.isSelected = true refreshDeviceList() } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } 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: - 刷新设备列表 private func refreshDeviceList() { let count = selectedTabIndex == 0 ? 30 : (selectedTabIndex == 1 ? 10 : 20) var cellModels = [TPBBaseTableCellModel]() for i in 0..<count { let cell = DeviceListCell(frame: .zero) cell.configure(with: "设备 \(i + 1)") let cellModel = TPBBaseTableCellModel.customContent(with: cell.contentView) cellModel.height = TPBTableElementHeight.customHeight(80) cellModels.append(cellModel) } let section = TPBTableSectionModel() section.cellModelArray = cellModels 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) // 初始化设备列表 refreshDeviceList() // 合并 sections if sectionArray.count <= 1 { sectionArray = tempSectionArray + sectionArray } else { sectionArray[0] = tempSectionArray[0] } 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() { } @objc private func onLocationTapped() { } @objc private func onSearchTapped() { } } // 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 } }
最新发布
12-03
import UIKit import SnapKit class DeviceListNewViewController: SurveillanceCommonTableController { // MARK: - 属性 private lazy var titleView: UILabel = { let label = UILabel() label.text = "设备列表" label.font = .tpm20Medium() label.textColor = .tpbTextPrimary label.textAlignment = .center return label }() private lazy var searchBar: TPBSearchBar = { let bar = TPBSearchBar() bar.placeholder = "搜索" bar.backgroundColor = .clear bar.delegate = self return bar }() private lazy var searchContainerView: UIView = { let view = UIView() view.backgroundColor = UIColor(white: 0.96, alpha: 1.0) view.layer.cornerRadius = 22 view.clipsToBounds = true view.addSubview(searchBar) return view }() private lazy var tabScrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = .clear scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() private lazy var tabButtons: [UIButton] = { return tabButtonTitles.enumerated().map { index, title in let btn = UIButton(type: .custom) btn.tag = index btn.setTitle(title, for: .normal) btn.titleLabel?.font = UIFont.tpr14Regular() btn.setTitleColor(.black, for: .normal) btn.setTitleColor(.tpbPrimary, 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 let tabButtonTitles = ["所有设备", "收藏页面", "站点选择"] private var selectedTabIndex = 0 // MARK: - View Setup override func tpbSetupSubviews() { super.tpbSetupSubviews() setupNav() view.addSubview(titleView) view.addSubview(searchContainerView) view.addSubview(tabScrollView) // 初始化按钮选中状态 updateTabSelection(index: selectedTabIndex) // 👇 直接在这里布局 tab 按钮(内联实现,避免找到方法) layoutTabButtonsInScrollView() } private func setupNav() { navigationItem.leftBarButtonItem = tpbCreateLeftBarButtonItem( with: TPImageLiteral("icon_back"), andTarget: self, andAction: #selector(onBack) ) navigationItem.rightBarButtonItem = tpbCreateRightBarButtonItem( with: TPImageLiteral("icon_settings"), andTarget: self, andAction: #selector(onSettings) ) navigationItem.title = "" } override func tpbMakeConstraint() { super.tpbMakeConstraint() titleView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).offset(8) make.centerX.equalToSuperview() } searchContainerView.snp.makeConstraints { make in make.top.equalTo(titleView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(44) // 固定高度适配 TPBSearchBar 常见大小 } searchBar.snp.makeConstraints { make in make.edges.equalTo(searchContainerView).inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) } tabScrollView.snp.makeConstraints { make in make.top.equalTo(searchContainerView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(0) make.height.equalTo(40) } } // MARK: - 内联布局 Tab 按钮(防止 “Cannot find” 错误) private func layoutTabButtonsInScrollView() { // 清空之前的内容 tabScrollView.subviews.forEach { $0.removeFromSuperview() } var previousButton: UIButton? var currentX: CGFloat = 16 for button in tabButtons { tabScrollView.addSubview(button) // 使用固定 frame 或 SnapKit 设置位置 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.contentLayoutGuide).offset(16) } make.width.greaterThanOrEqualTo(80) } previousButton = button currentX += button.intrinsicContentSize.width + 8 } // 手动设置 contentSize(更稳定) DispatchQueue.main.async { var totalWidth: CGFloat = 16 for (i, btn) in self.tabButtons.enumerated() { totalWidth += btn.intrinsicContentSize.width + (i > 0 ? 8 : 0) } totalWidth += 16 self.tabScrollView.contentSize = CGSize(width: totalWidth, height: 40) } } // MARK: - 数据模型构建(仅搭建结构,填充真实数据) private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() let section0 = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() // Cell 1: Tab ScrollView let tabCellModel = TPBBaseTableCellModel.customContent(with: tabScrollView) cellModels.append(tabCellModel) // Cell 2: 占位用的 CollectionView 区域(仅视觉框架) let collectionViewPlaceholder = UIView() collectionViewPlaceholder.backgroundColor = .tpbBackground collectionViewPlaceholder.layer.cornerRadius = 12 collectionViewPlaceholder.clipsToBounds = true // 添加一个简单的提示标签表示“这里是列表” let placeholderLabel = UILabel() placeholderLabel.text = "设备内容区域" placeholderLabel.textColor = .tpbTextSecondaryContent placeholderLabel.font = .tpr14Regular() placeholderLabel.textAlignment = .center placeholderLabel.sizeToFit() collectionViewPlaceholder.addSubview(placeholderLabel) placeholderLabel.snp.makeConstraints { make in make.center.equalToSuperview() } let collectionCellModel = TPBBaseTableCellModel.customContent(with: collectionViewPlaceholder) collectionCellModel.height = TPBTableElementHeight.customHeight(240) cellModels.append(collectionCellModel) section0.cellModelArray = cellModels tempSectionArray.append(section0) sectionArray = tempSectionArray tableView.reloadData() } // MARK: - 生命周期 override func viewDidLoad() { super.viewDidLoad() reloadData() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 后续绑定 collectionView delegate } // MARK: - 点击事件 @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 // 后续刷新数据 print("Switched to tab: \(selectedTabIndex)") } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } } // MARK: - TPBSearchBarDelegate(空实现,仅满足协议要求) extension DeviceListNewViewController: TPBSearchBarDelegate { func searchBar(_ searchBar: TPBSearchBar, textDidChange searchText: String) { // TODO: 搜索关键词变化时处理(后续接入 ViewModel) } func searchBarTextDidBeginEditing(_ searchBar: TPBSearchBar) { addTapToDismissKeyboard() } func searchBarTextDidEndEditing(_ searchBar: TPBSearchBar) { removeTapToDismissKeyboard() } func searchBarSearchButtonClicked(_ searchBar: TPBSearchBar) { searchBar.resignFirstResponder() } } // MARK: - Keyboard Dismiss Gesture private extension DeviceListNewViewController { func addTapToDismissKeyboard() { let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tap.cancelsTouchesInView = false view.addGestureRecognizer(tap) objc_setAssociatedObject( self, &AssociatedKeys.searchBarTapGestureKey, tap, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } func removeTapToDismissKeyboard() { guard let tap = objc_getAssociatedObject(self, &AssociatedKeys.searchBarTapGestureKey) as? UITapGestureRecognizer else { return } view.removeGestureRecognizer(tap) objc_setAssociatedObject(self, &AssociatedKeys.searchBarTapGestureKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } @objc func dismissKeyboard() { searchBar.resignFirstResponder() } } // MARK: - Associated Object Keys private struct AssociatedKeys { static var searchBarTapGestureKey: UInt8 = 0 } 现在这个,给我实现成这样的效果: 屏幕靠上部分显示titleview,然后下面显示searchbar,然后下面显示收藏按钮、所有设备那个scrollview,然后在下面显示一个UIcollectionView,滚动范围就在这个UICollectionView区域,明白我想要的结构了吗?修改代码 import UIKit import SnapKit class DeviceListNewViewController: SurveillanceCommonTableController { // MARK: - 属性 private lazy var titleView: UILabel = { let label = UILabel() label.text = "设备列表" label.font = .tpm20Medium() label.textColor = .tpbTextPrimary label.textAlignment = .center return label }() private lazy var searchBar: TPBSearchBar = { let bar = TPBSearchBar() bar.placeholder = "搜索" bar.backgroundColor = .clear bar.delegate = self return bar }() private lazy var searchContainerView: UIView = { let view = UIView() view.backgroundColor = UIColor(white: 0.96, alpha: 1.0) view.layer.cornerRadius = 22 view.clipsToBounds = true view.addSubview(searchBar) return view }() private lazy var tabScrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = .clear scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() private lazy var tabButtons: [UIButton] = { return tabButtonTitles.enumerated().map { index, title in let btn = UIButton(type: .custom) btn.tag = index btn.setTitle(title, for: .normal) btn.titleLabel?.font = UIFont.tpr14Regular() btn.setTitleColor(.black, for: .normal) btn.setTitleColor(.tpbPrimary, 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 let tabButtonTitles = ["所有设备", "收藏页面", "站点选择"] private var selectedTabIndex = 0 // MARK: - View Setup override func tpbSetupSubviews() { super.tpbSetupSubviews() setupNav() view.addSubview(titleView) view.addSubview(searchContainerView) view.addSubview(tabScrollView) // 初始化按钮选中状态 updateTabSelection(index: selectedTabIndex) // 👇 直接在这里布局 tab 按钮(内联实现,避免找到方法) layoutTabButtonsInScrollView() } private func setupNav() { navigationItem.leftBarButtonItem = tpbCreateLeftBarButtonItem( with: TPImageLiteral("icon_back"), andTarget: self, andAction: #selector(onBack) ) navigationItem.rightBarButtonItem = tpbCreateRightBarButtonItem( with: TPImageLiteral("icon_settings"), andTarget: self, andAction: #selector(onSettings) ) navigationItem.title = "" } override func tpbMakeConstraint() { super.tpbMakeConstraint() titleView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).offset(8) make.centerX.equalToSuperview() } searchContainerView.snp.makeConstraints { make in make.top.equalTo(titleView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(44) // 固定高度适配 TPBSearchBar 常见大小 } searchBar.snp.makeConstraints { make in make.edges.equalTo(searchContainerView).inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) } tabScrollView.snp.makeConstraints { make in make.top.equalTo(searchContainerView.snp.bottom).offset(16) make.leading.trailing.equalToSuperview().inset(0) make.height.equalTo(40) } } // MARK: - 内联布局 Tab 按钮(防止 “Cannot find” 错误) private func layoutTabButtonsInScrollView() { // 清空之前的内容 tabScrollView.subviews.forEach { $0.removeFromSuperview() } var previousButton: UIButton? var currentX: CGFloat = 16 for button in tabButtons { tabScrollView.addSubview(button) // 使用固定 frame 或 SnapKit 设置位置 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.contentLayoutGuide).offset(16) } make.width.greaterThanOrEqualTo(80) } previousButton = button currentX += button.intrinsicContentSize.width + 8 } // 手动设置 contentSize(更稳定) DispatchQueue.main.async { var totalWidth: CGFloat = 16 for (i, btn) in self.tabButtons.enumerated() { totalWidth += btn.intrinsicContentSize.width + (i > 0 ? 8 : 0) } totalWidth += 16 self.tabScrollView.contentSize = CGSize(width: totalWidth, height: 40) } } // MARK: - 数据模型构建(仅搭建结构,填充真实数据) private func reloadData() { var tempSectionArray = [TPBTableSectionModel]() let section0 = TPBTableSectionModel() var cellModels = [TPBBaseTableCellModel]() // Cell 1: Tab ScrollView let tabCellModel = TPBBaseTableCellModel.customContent(with: tabScrollView) cellModels.append(tabCellModel) // Cell 2: 占位用的 CollectionView 区域(仅视觉框架) let collectionViewPlaceholder = UIView() collectionViewPlaceholder.backgroundColor = .tpbBackground collectionViewPlaceholder.layer.cornerRadius = 12 collectionViewPlaceholder.clipsToBounds = true // 添加一个简单的提示标签表示“这里是列表” let placeholderLabel = UILabel() placeholderLabel.text = "设备内容区域" placeholderLabel.textColor = .tpbTextSecondaryContent placeholderLabel.font = .tpr14Regular() placeholderLabel.textAlignment = .center placeholderLabel.sizeToFit() collectionViewPlaceholder.addSubview(placeholderLabel) placeholderLabel.snp.makeConstraints { make in make.center.equalToSuperview() } let collectionCellModel = TPBBaseTableCellModel.customContent(with: collectionViewPlaceholder) collectionCellModel.height = TPBTableElementHeight.customHeight(240) cellModels.append(collectionCellModel) section0.cellModelArray = cellModels tempSectionArray.append(section0) sectionArray = tempSectionArray tableView.reloadData() } // MARK: - 生命周期 override func viewDidLoad() { super.viewDidLoad() reloadData() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 后续绑定 collectionView delegate } // MARK: - 点击事件 @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 // 后续刷新数据 print("Switched to tab: \(selectedTabIndex)") } private func updateTabSelection(index: Int) { for (i, btn) in tabButtons.enumerated() { btn.isSelected = (i == index) } } } // MARK: - TPBSearchBarDelegate(空实现,仅满足协议要求) extension DeviceListNewViewController: TPBSearchBarDelegate { func searchBar(_ searchBar: TPBSearchBar, textDidChange searchText: String) { // TODO: 搜索关键词变化时处理(后续接入 ViewModel) } func searchBarTextDidBeginEditing(_ searchBar: TPBSearchBar) { addTapToDismissKeyboard() } func searchBarTextDidEndEditing(_ searchBar: TPBSearchBar) { removeTapToDismissKeyboard() } func searchBarSearchButtonClicked(_ searchBar: TPBSearchBar) { searchBar.resignFirstResponder() } } // MARK: - Keyboard Dismiss Gesture private extension DeviceListNewViewController { func addTapToDismissKeyboard() { let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tap.cancelsTouchesInView = false view.addGestureRecognizer(tap) objc_setAssociatedObject( self, &AssociatedKeys.searchBarTapGestureKey, tap, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) } func removeTapToDismissKeyboard() { guard let tap = objc_getAssociatedObject(self, &AssociatedKeys.searchBarTapGestureKey) as? UITapGestureRecognizer else { return } view.removeGestureRecognizer(tap) objc_setAssociatedObject(self, &AssociatedKeys.searchBarTapGestureKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } @objc func dismissKeyboard() { searchBar.resignFirstResponder() } } // MARK: - Associated Object Keys private struct AssociatedKeys { static var searchBarTapGestureKey: UInt8 = 0 }
12-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值