当视图含有滚动视图的时候使用flex布局注意
可滚动元素一定要加到contentView.rootView上否则不能触发sizeEvent
完整代码
AgentShare.swift
//
// AgentShare.swift
// YBLiveOne
//
// Created by iyz on 2026/9/22.
// Copyright © 2026 iOS. All rights reserved.
//
import FlexLayout
import PinLayout
import Photos
import SDWebImage
import SwiftyJSON
import UIKit
class AgentShare: BaseFlexView {
private var titiL = UILabel()
private var styleTitL = UILabel()
private var scrollView = UIScrollView()
private var contentView = BaseFlexView(.adjustWidth)
private var shareTitL = UILabel()
private let shareContentView = UIView()
private let styleItemMarginLeft: CGFloat = 10
private var styleContentWidth: CGFloat = 0
private var styleItems: [AgentStyleItem] = []
private var selectedThumb = ""
private let shareItemHeight: CGFloat = 70
private let shareColumnCount: CGFloat = 5
override func initUI() {
let tap = UITapGestureRecognizer(target: self, action: #selector(dissmiss))
tap.delegate = self
addGestureRecognizer(tap)
titiL.text = i18n("分享赚钱")
titiL.textColor = .hex("000")
titiL.font = kFont(size: 18)
styleTitL.text = i18n("分享样式")
styleTitL.textColor = .hex("000")
styleTitL.font = kFont(size: 15)
shareTitL.text = i18n("分享至")
shareTitL.textColor = .hex("000")
shareTitL.font = kFont(size: 15)
backgroundColor = .hex("000", 0.4)
rootView.backgroundColor = .hex("#fff")
rootView.layer.cornerRadius = 15
rootView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
rootView.flex.paddingTop(20).paddingBottom(kSafeBotElement).define { flex in
//
flex.addItem().alignItems(.center).define { flex in
flex.addItem(titiL)
}
//
flex.addItem().height(44).justifyContent(.center).paddingHorizontal(15).marginTop(10).define { flex in
flex.view?.backgroundColor = .hex("f6f6f6")
flex.addItem(styleTitL)
}
//
flex.addItem(scrollView).width(100%).height(AgentStyleItem.layoutHeight)
//
flex.addItem().height(44).justifyContent(.center).paddingHorizontal(15).define { flex in
flex.view?.backgroundColor = .hex("f6f6f6")
flex.addItem(shareTitL)
}
flex.addItem(shareContentView)
}
scrollView.addSubview(contentView)
configureShareOptions()
}
@objc func configData() {
ApiService.share.post(url: "Agent.getPosterList", parameter: nil) { [weak self] code, info, msg in
guard code == 0 else {
if code != 700, !msg.isEmpty {
kToastHud(msg)
}
return
}
self?.updateStyleItems(JSON(info).arrayValue)
} fail: {}
}
private func updateStyleItems(_ lists: [JSON]) {
contentView.subviews
.filter { $0 !== contentView.rootView }
.forEach { $0.removeFromSuperview() }
styleContentWidth = 0
styleItems = []
if !lists.contains(where: { $0["thumb"].stringValue == selectedThumb }) {
selectedThumb = lists.first?["thumb"].stringValue ?? ""
}
contentView.flex.direction(.row).define { flex in
for list in lists {
let agentItem = AgentStyleItem(.adjustHeight)
agentItem.dataDic = list
agentItem.isSelected = list["thumb"].stringValue == selectedThumb
agentItem.onSelect = { [weak self] item in
self?.selectPoster(item)
}
flex.addItem(agentItem).width(AgentStyleItem.layoutWidth).marginLeft(styleItemMarginLeft)
styleContentWidth += styleItemMarginLeft + AgentStyleItem.layoutWidth
styleItems.append(agentItem)
}
}
layout()
}
override func layout() {
rootView.pin.bottom().left().width(100%)
rootView.flex.layout(mode: .adjustHeight)
let contentSize = CGSize(width: max(scrollView.bounds.width, styleContentWidth), height: AgentStyleItem.layoutHeight)
contentView.pin.topLeft().size(contentSize)
contentView.flex.layout(mode: .fitContainer)
scrollView.contentSize = contentSize
}
private func configureShareOptions() {
var platforms:[String] = Common.shareType.compactMap { $0.string }.filter { !$0.isEmpty }
if !platforms.contains("save") {
platforms.append("save")
}
let rowCount = max(1, Int(ceil(CGFloat(platforms.count) / shareColumnCount)))
let itemWidthPercent = flexPercent(100 / shareColumnCount)
shareContentView.flex.direction(.row).wrap(.wrap).paddingVertical(10).define { flex in
for platform in platforms {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "liveShare_\(platform)"), for: .normal)
button.accessibilityLabel = shareTitle(for: platform)
button.addTarget(self, action: #selector(shareButtonTapped(_:)), for: .touchUpInside)
button.accessibilityIdentifier = platform
flex.addItem(button).width(itemWidthPercent).height(shareItemHeight)
}
}
shareContentView.flex.height(CGFloat(rowCount) * shareItemHeight + 20)
}
private func flexPercent(_ value: CGFloat) -> FPercent {
value%
}
private func selectPoster(_ item: AgentStyleItem) {
selectedThumb = item.dataDic["thumb"].stringValue
styleItems.forEach { $0.isSelected = $0 === item }
}
@objc private func shareButtonTapped(_ sender: UIButton) {
guard let platform = sender.accessibilityIdentifier else { return }
if platform == "save" {
savePoster()
} else {
guard !selectedThumb.isEmpty else {
kToastHud(i18n("请选择分享样式"))
return
}
YBShareManager.sharePoster(selectedThumb, platform: platform)
}
}
private func shareTitle(for platform: String) -> String {
switch platform {
case "wx": return i18n("微信")
case "wchat": return i18n("朋友圈")
case "qzone": return i18n("QQ空间")
case "qq": return "QQ"
case "facebook": return "Facebook"
case "twitter": return i18n("推特")
case "save": return i18n("保存图片")
default: return platform
}
}
private func savePoster() {
guard let url = URL(string: selectedThumb) else {
kToastHud(i18n("请选择分享样式"))
return
}
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
guard self != nil, let data, let image = UIImage(data: data) else {
DispatchQueue.main.async { kToastHud(i18n("保存图片失败")) }
return
}
self?.save(image: image)
}.resume()
}
private func save(image: UIImage) {
let saveImage = { [weak self] in
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}) { success, _ in
DispatchQueue.main.async {
kToastHud(i18n(success ? "保存图片成功" : "保存图片失败"))
self?.setNeedsLayout()
}
}
}
switch PHPhotoLibrary.authorizationStatus() {
case .authorized, .limited:
saveImage()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized || status == .limited {
saveImage()
} else {
DispatchQueue.main.async { kToastHud(i18n("没有相册权限")) }
}
}
default:
DispatchQueue.main.async { kToastHud(i18n("没有相册权限")) }
}
}
@objc func dissmiss() {
subviews.forEach { $0.removeFromSuperview() }
removeFromSuperview()
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
///
class AgentStyleItem: BaseFlexView {
static let layoutWidth: CGFloat = 130
static let imageAspectRatio: CGFloat = 0.667
static let verticalPadding: CGFloat = 10
static let buttonMarginTop: CGFloat = 10
static let buttonHeight: CGFloat = 24
static var imageHeight: CGFloat { layoutWidth / imageAspectRatio }
static var layoutHeight: CGFloat {
verticalPadding * 2 + imageHeight + buttonMarginTop + buttonHeight
}
private var imgIV = UIImageView(.fit)
private var flagBtn = BaseButton(type: .custom)
var onSelect: ((AgentStyleItem) -> Void)?
var dataDic: JSON = [:] {
didSet { configData() }
}
override var isSelected: Bool {
didSet {
flagBtn.isSelected = isSelected
}
}
override func initUI() {
addTarget(self, action: #selector(clickItem), for: .touchUpInside)
rootView.isUserInteractionEnabled = false
flagBtn.setTitleColor(.hex("#4998F5"), for: .normal)
flagBtn.setTitle(i18n("使用"), for: .normal)
flagBtn.jk_setBackgroundColor(.hex("fff"), for: .normal)
flagBtn.setTitleColor(.hex("#fff"), for: .selected)
flagBtn.setTitle(i18n("已选"), for: .selected)
flagBtn.jk_setBackgroundColor(.hex("4998F5"), for: .selected)
flagBtn.layer.borderColor = UIColor.hex("#4998F5").cgColor
flagBtn.layer.borderWidth = 1
flagBtn.titleLabel?.font = kFont(size: 12)
flagBtn.contentEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
imgIV.layer.masksToBounds = true
rootView.flex.direction(.column).alignItems(.center).paddingVertical(Self.verticalPadding).define { flex in
flex.addItem(imgIV).width(Self.layoutWidth).height(Self.imageHeight)
flex.addItem(flagBtn).marginTop(Self.buttonMarginTop).height(Self.buttonHeight).define { flex in
flex.view?.layer.cornerRadius = 12
flex.view?.layer.masksToBounds = true
}
}
}
func configData() {
imgIV.sd_setImage(with: URL(string: dataDic["thumb"].stringValue))
layout()
}
@objc func clickItem(it: AgentStyleItem) {
onSelect?(it)
}
}
///
class AgentShareItem: BaseFlexView {}
///
extension AgentShare: UIGestureRecognizerDelegate {
// gesture delegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let tView = touch.view {
if tView.isDescendant(of: rootView) {
return false
}
}
return true
}
}
AgentShare2.swift
//
// AgentShare2.swift
// YBLiveOne
//
// 使用 BaseFlexView2.sizeEvent 驱动 UIScrollView contentSize 的独立实现。
//
import FlexLayout
import PinLayout
import SwiftyJSON
import UIKit
final class AgentShare2: BaseFlexView2 {
private let titleLabel = UILabel()
private let styleTitleLabel = UILabel()
private let shareTitleLabel = UILabel()
private let scrollView = UIScrollView()
private let contentView = BaseFlexView2(.adjustWidth)
private let shareContentView = UIView()
private let itemMarginLeft: CGFloat = 10
private let shareItemHeight: CGFloat = 70
private let shareColumnCount: CGFloat = 5
private var styleItems: [AgentStyleItem] = []
private var selectedThumb = ""
override func initUI() {
backgroundColor = .hex("000", 0.4)
rootView.backgroundColor = .hex("fff")
rootView.layer.cornerRadius = 15
rootView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
configureLabels()
configureScrollView()
configureShareOptions()
rootView.flex.paddingTop(20).paddingBottom(kSafeBotElement).define { flex in
flex.addItem().alignItems(.center).define { flex in
flex.addItem(titleLabel)
}
flex.addItem().height(44).justifyContent(.center).paddingHorizontal(15).marginTop(10).define { flex in
flex.view?.backgroundColor = .hex("f6f6f6")
flex.addItem(styleTitleLabel)
}
flex.addItem(scrollView).width(100%).height(AgentStyleItem.layoutHeight)
flex.addItem().height(44).justifyContent(.center).paddingHorizontal(15).define { flex in
flex.view?.backgroundColor = .hex("f6f6f6")
flex.addItem(shareTitleLabel)
}
flex.addItem(shareContentView)
}
}
@objc func configData() {
ApiService.share.post(url: "Agent.getPosterList", parameter: nil) { [weak self] code, info, msg in
guard code == 0 else {
if code != 700, !msg.isEmpty {
kToastHud(msg)
}
return
}
// self?.updateStyleItems(JSON(info).arrayValue)
var listsss = JSON(info).arrayValue
listsss.append(contentsOf: JSON(info).arrayValue)
listsss.append(contentsOf: JSON(info).arrayValue)
listsss.append(contentsOf: JSON(info).arrayValue)
self?.updateStyleItems(listsss)
} fail: {}
}
/// 在接口数据返回后调用;滚动宽度由 contentView 的 sizeEvent 自动更新。
func updateStyleItems(_ lists: [JSON]) {
styleItems.forEach { $0.removeFromSuperview() }
styleItems.removeAll()
if !lists.contains(where: { $0["thumb"].stringValue == selectedThumb }) {
selectedThumb = lists.first?["thumb"].stringValue ?? ""
}
// BaseFlexView2 实际参与 .adjustWidth 测量的是 rootView,
// 海报项必须添加到 rootView.flex,sizeEvent 才能拿到内容总宽度。
contentView.rootView.flex.direction(.row).define { flex in
for data in lists {
let item = AgentStyleItem(.adjustHeight)
item.dataDic = data
item.isSelected = data["thumb"].stringValue == selectedThumb
item.onSelect = { [weak self] selectedItem in
self?.selectPoster(selectedItem)
}
flex.addItem(item)
.width(AgentStyleItem.layoutWidth)
.marginLeft(itemMarginLeft)
styleItems.append(item)
}
}
setNeedsLayout()
}
override func layout() {
rootView.pin.bottom().left().width(100%)
rootView.flex.layout(mode: .adjustHeight)
// 【可不加此行】先给 contentView 一个可用高度;实际宽度随后由 .adjustWidth 计算。
contentView.pin.topLeft()
.width(scrollView.bounds.width)
.height(AgentStyleItem.layoutHeight)
contentView.layout()
}
private func configureLabels() {
titleLabel.text = i18n("分享赚钱")
titleLabel.textColor = .hex("000")
titleLabel.font = kFont(size: 18)
styleTitleLabel.text = i18n("分享样式")
styleTitleLabel.textColor = .hex("000")
styleTitleLabel.font = kFont(size: 15)
shareTitleLabel.text = i18n("分享至")
shareTitleLabel.textColor = .hex("000")
shareTitleLabel.font = kFont(size: 15)
}
private func configureScrollView() {
scrollView.showsHorizontalScrollIndicator = false
scrollView.addSubview(contentView)
contentView.sizeEvent = { [weak self] contentWidth in
guard let self else { return }
let width = max(scrollView.bounds.width, contentWidth)
contentView.pin.width(width).height(AgentStyleItem.layoutHeight)
scrollView.contentSize = CGSize(width: width, height: AgentStyleItem.layoutHeight)
}
}
private func configureShareOptions() {
var platforms = Common.shareType.compactMap { $0.string }.filter { !$0.isEmpty }
if !platforms.contains("save") {
platforms.append("save")
}
let rowCount = max(1, Int(ceil(CGFloat(platforms.count) / shareColumnCount)))
shareContentView.flex.direction(.row).wrap(.wrap).paddingVertical(10).define { flex in
for platform in platforms {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "liveShare_\(platform)"), for: .normal)
flex.addItem(button).width((100 / shareColumnCount)%).height(shareItemHeight)
}
}
shareContentView.flex.height(CGFloat(rowCount) * shareItemHeight + 20)
}
private func selectPoster(_ item: AgentStyleItem) {
selectedThumb = item.dataDic["thumb"].stringValue
styleItems.forEach { $0.isSelected = $0 === item }
}
}
BaseFlexView2.swift
//
// BaseFlexView2.swift
// YBLiveOne
//
// 独立的 Flex 容器:尺寸发生变化时通过 sizeEvent 回传最终宽度或高度。
//
import FlexLayout
import PinLayout
import UIKit
typealias FlexSizeEvent2 = (_ value: CGFloat) -> Void
class BaseFlexView2: UIControl {
var layoutMode: Flex.LayoutMode
var rootView = UIView()
var sizeEvent: FlexSizeEvent2?
private var lastSize: CGFloat = .zero
init(_ mode: Flex.LayoutMode = .adjustHeight) {
layoutMode = mode
super.init(frame: .zero)
commonInit()
}
required init?(coder: NSCoder) {
layoutMode = .fitContainer
super.init(coder: coder)
commonInit()
}
override init(frame: CGRect) {
layoutMode = .fitContainer
super.init(frame: frame)
commonInit()
}
private func commonInit() {
initUI()
addSubview(rootView)
}
func initUI() {}
func layout() {
switch layoutMode {
case .adjustWidth:
rootView.pin.top().left().height(100%)
rootView.flex.layout(mode: .adjustWidth)
notifySizeIfNeeded(rootView.bounds.width)
case .adjustHeight:
rootView.pin.top().right().width(100%)
rootView.flex.layout(mode: .adjustHeight)
notifySizeIfNeeded(rootView.bounds.height)
default:
rootView.pin.top().right().width(100%).height(100%)
rootView.flex.layout(mode: layoutMode)
}
}
override func layoutSubviews() {
super.layoutSubviews()
layout()
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
layout()
return rootView.bounds.size
}
private func notifySizeIfNeeded(_ value: CGFloat) {
guard abs(lastSize - value) > .ulpOfOne else { return }
lastSize = value
sizeEvent?(value)
}
}