角色模块UI

This commit is contained in:
mh 2025-10-16 16:05:19 +08:00
parent 72b9d15aa3
commit 9fb598f906
51 changed files with 1054 additions and 2 deletions

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_check_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_check_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_discord_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_discord_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_money_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_money_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_not_subscriber_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_not_subscriber_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_search_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_search_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_subscriber_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_subscriber_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_book@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_book@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_bottom_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_bottom_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_close_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_close_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_from@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_from@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_open_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_open_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_star@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_star@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_top_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_top_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_video@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_video@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,74 @@
//
// CLContainerVC.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/15.
//
import Foundation
// tabbarrootvcheader vc
final class CLContainerVC: UIViewController {
private let header = HeaderView() //
var nav = CLNavigationController() //
init(rootvc: UIViewController) {
super.init(nibName: nil, bundle: nil)
nav = CLNavigationController(rootViewController: rootvc)
addChild(nav)
// nav.setViewControllers([rootViewController], animated: false)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[header, nav.view].forEach { v in
// v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
header.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(88)
}
nav.view.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(header.snp.bottom)
make.bottom.equalToSuperview()
}
}
}
final class HeaderView: UIView {
lazy var avatar: UIImageView = {
let imageview = UIImageView()
imageview.backgroundColor = .blue
return imageview
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .red
setup()
}
required init?(coder: NSCoder) { fatalError() }
func render(_ model: AnyObject) {
// avatar.image = model.avatar
}
private func setup() {
/* */
addSubview(avatar)
avatar.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(30)
make.width.height.equalTo(44)
}
}
}

View File

@ -8,7 +8,7 @@
import UIKit
class CLTabRootController<Container: UIView>: CLViewController<Container>{
private var bgTopIv: UIImageView!
var bgTopIv: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()

View File

@ -0,0 +1,189 @@
//
// CLTopHeaderView.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/15.
//
import Foundation
import Combine
///
enum JumpTarget: CaseIterable {
case search
case check //
case discord
// case web(url: String) //
}
/// tabbarrootvc
class CLTopHeaderView: UIView {
private let subject = PassthroughSubject<JumpTarget, Never>()
var jumpPublisher: AnyPublisher<JumpTarget, Never> {
subject.eraseToAnyPublisher()
}
lazy var avatarContainerView: UIView = {
let view = UIView()
return view
}()
lazy var avatar: UIImageView = {
let imgView = UIImageView()
imgView.contentMode = .scaleAspectFill
imgView.cornerRadius = 18.0
imgView.backgroundColor = .purple
return imgView
}()
lazy var vipImgView: UIImageView = {
let imgview = UIImageView(image: UIImage(named: "header_not_subscriber_icon"))
imgview.contentMode = .scaleAspectFill
return imgview
}()
lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 18.0
return view
}()
lazy var starImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "header_money_icon"))
imgView.contentMode = .scaleAspectFill
return imgView
}()
lazy var moneyLab: UILabel = {
let lab = UILabel()
lab.text = "150"
lab.font = UIFont.boldSystemFont(ofSize: 14)
return lab
}()
lazy var searchImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "header_search_icon"))
imgView.contentMode = .scaleAspectFill
imgView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(searchImgViewClicked))
imgView.addGestureRecognizer(tap)
return imgView
}()
lazy var calendarImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "header_check_icon"))
imgView.contentMode = .scaleAspectFill
imgView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(calendarImgViewClicked))
imgView.addGestureRecognizer(tap)
return imgView
}()
lazy var discordImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "header_discord_icon"))
imgView.contentMode = .scaleAspectFill
imgView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(discordImgViewClicked))
imgView.addGestureRecognizer(tap)
return imgView
}()
lazy var avatarStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [avatarContainerView, containerView])
stackView.axis = .horizontal
stackView.spacing = 15.0
stackView.alignment = .fill
stackView.distribution = .fill
return stackView
}()
lazy var toolStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [searchImgView, calendarImgView, discordImgView])
stackView.axis = .horizontal
stackView.spacing = 15.0
stackView.alignment = .fill
stackView.distribution = .fill
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Action
@objc func searchImgViewClicked() {
print("111111")
// UIWindow.getTopViewController()?.navigationController?.pushViewController(TestEntrancesController(), animated: true)
subject.send(.search)
}
@objc func calendarImgViewClicked() {
print("2222")
subject.send(.check)
}
@objc func discordImgViewClicked() {
print("333")
subject.send(.discord)
}
// MARK: Subviews
func setupSubviews() {
addSubview(avatarStackView)
addSubview(toolStackView)
avatarContainerView.addSubview(avatar)
avatarContainerView.addSubview(vipImgView)
containerView.addSubview(starImgView)
containerView.addSubview(moneyLab)
avatarStackView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(20)
make.top.equalToSuperview().offset(UIDevice().statusBarHeight + 4.0)
}
toolStackView.snp.makeConstraints { make in
make.right.equalToSuperview().inset(20)
make.centerY.equalTo(avatarStackView.snp.centerY)
}
avatarContainerView.snp.makeConstraints { make in
make.width.height.equalTo(36)
}
avatar.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
vipImgView.snp.makeConstraints { make in
make.right.bottom.equalToSuperview()
}
containerView.snp.makeConstraints { make in
make.height.equalTo(36)
}
starImgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(6.5)
make.centerY.equalToSuperview()
}
moneyLab.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(starImgView.snp.right).offset(5.5)
make.right.equalToSuperview().inset(12.5)
}
}
}

View File

@ -0,0 +1,61 @@
//
// RolesRootPageController.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/14.
//
import UIKit
import Combine
class RolesRootPageController: CLTabRootController<RolesRootPageView> {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
let baseImageView = UIImageView(image: R.image.base_bg())
baseImageView.contentMode = .scaleAspectFill
self.view.insertSubview(baseImageView, at: 0)
baseImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
view.backgroundColor = .clear
bgTopIv.isHidden = true
setupViews()
setupEvent()
}
private func setupViews() {
navigationView.bgView.alpha = 0
navigationView.setupBgViewToStatusBarHeight()
navigationView.isUserInteractionEnabled = false
}
private func setupEvent() {
self.container.jumpPublisher
.receive(on: RunLoop.main)
.sink { [weak self] target in
self?.handleJump(target)
}
.store(in: &cancellables)
}
//
private func handleJump(_ target: JumpTarget) {
switch target {
case .search:
let vc = TestEntrancesController()
navigationController?.pushViewController(vc, animated: true)
case .check:
let vc = TestEntrancesController()
navigationController?.pushViewController(vc, animated: true)
case .discord:
let vc = TestEntrancesController()
navigationController?.pushViewController(vc, animated: true)
}
}
}

View File

@ -0,0 +1,186 @@
//
// CLRoleCollectionCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/15.
//
import UIKit
class CLRoleCollectionCell: UICollectionViewCell {
let cellWidth = (UIScreen.width - 30.0) / 2.0
lazy var bookBgImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_book"))
imgView.contentMode = .scaleAspectFill
return imgView
}()
lazy var fromImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_from"))
imgView.contentMode = .scaleAspectFill
return imgView
}()
lazy var coverImgView: UIImageView = {
let imgView = UIImageView()
imgView.contentMode = .scaleAspectFill
imgView.backgroundColor = .red
imgView.cornerRadius = 25.0
return imgView
}()
lazy var topShadowImgView: UIImageView = {
let image = UIImage(named: "role_top_bg")
let stretchedImage = image?.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5)
let imgView = UIImageView(image: stretchedImage)
return imgView
}()
lazy var bottomShadowImgView: UIImageView = {
let image = UIImage(named: "role_bottom_bg")
let stretchedImage = image?.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5)
let imgView = UIImageView(image: stretchedImage)
return imgView
}()
lazy var starImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_star"))
imgView.contentMode = .scaleAspectFill
return imgView
}()
lazy var sourceLab: UILabel = {
let lab = UILabel()
lab.font = UIFont.boldSystemFont(ofSize: 15)
lab.text = "9.5"
lab.textColor = UIColor(hex: "#FFE100")
return lab
}()
lazy var nameLab: UILabel = {
let lab = UILabel()
lab.font = UIFont.boldSystemFont(ofSize: 14)
lab.text = "Character · 18"
lab.textColor = UIColor(hex: "#FFFFFF")
return lab
}()
lazy var tagLab: UILabel = {
let lab = UILabel()
lab.font = UIFont.boldSystemFont(ofSize: 10)
lab.text = "# Sining ng Pagpapatalim / #xianxia / #swordsmanship"
lab.textColor = .white
lab.numberOfLines = 0
return lab
}()
lazy var descLab: UILabel = {
let lab = UILabel()
lab.numberOfLines = 3
lab.font = UIFont.systemFont(ofSize: 12)
lab.text = "Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast out"
lab.textColor = UIColor(hex: "#666666")
return lab
}()
lazy var remindLab: UILabel = {
let lab = UILabel()
lab.font = UIFont.boldSystemFont(ofSize: 10)
lab.text = "[The Lsat Oracle of Kael]"
lab.textColor = UIColor(hex: "#999999")
return lab
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.clipsToBounds = true
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: data
func setupData(desc: String) {
descLab.text = desc
}
// MARK: subviews
private func setupViews() {
contentView.addSubview(coverImgView)
contentView.addSubview(topShadowImgView)
contentView.addSubview(bottomShadowImgView)
contentView.addSubview(bookBgImgView)
contentView.addSubview(fromImgView)
contentView.addSubview(starImgView)
contentView.addSubview(sourceLab)
contentView.addSubview(nameLab)
contentView.addSubview(tagLab)
contentView.addSubview(descLab)
contentView.addSubview(remindLab)
coverImgView.snp.makeConstraints { make in
make.width.equalTo(cellWidth)
make.height.equalTo(coverImgView.snp.width).multipliedBy(4.0 / 3.0)
make.top.left.right.equalToSuperview()
}
topShadowImgView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalTo(sourceLab.snp.bottom).offset(18)
}
bottomShadowImgView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalTo(coverImgView.snp.bottom)
make.top.equalTo(nameLab.snp.top).offset(-25)
}
bookBgImgView.snp.makeConstraints { make in
make.top.left.equalToSuperview()
}
fromImgView.snp.makeConstraints { make in
make.top.left.equalToSuperview()
}
sourceLab.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.right.equalToSuperview().inset(18)
}
starImgView.snp.makeConstraints { make in
make.centerY.equalTo(sourceLab.snp.centerY)
make.right.equalTo(sourceLab.snp.left).offset(-5)
}
tagLab.snp.makeConstraints { make in
make.right.left.equalToSuperview().inset(10)
make.bottom.equalTo(coverImgView.snp.bottom).offset(-10)
}
nameLab.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(10)
make.bottom.equalTo(tagLab.snp.top).offset(-10)
}
descLab.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(10)
make.top.equalTo(coverImgView.snp.bottom).offset(10)
}
remindLab.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(10)
make.top.equalTo(descLab.snp.bottom).offset(5)
}
}
}

View File

@ -0,0 +1,78 @@
//
// CLWaterfallLayout.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/15.
//
import UIKit
final class CLWaterfallLayout: UICollectionViewLayout {
// MARK:
var columnCount: Int = 2
var columnSpacing: CGFloat = 15
var interItemSpacing: CGFloat = 10
var sectionInset: UIEdgeInsets = .init(top: 0, left: 10, bottom: 10, right: 10)
var delegate: WaterfallLayoutDelegate? = nil
// MARK:
private var cache: [UICollectionViewLayoutAttributes] = []
private var contentHeight: CGFloat = 0
private var colHeight: [CGFloat] = []
override var collectionViewContentSize: CGSize {
CGSize(width: collectionView!.bounds.width, height: contentHeight)
}
override func prepare() {
super.prepare()
guard cache.isEmpty, let cv = collectionView else { return }
contentHeight = sectionInset.top
colHeight = .init(repeating: sectionInset.top, count: columnCount)
let itemCount = cv.numberOfItems(inSection: 0)
let totalWidth = cv.bounds.width - sectionInset.left - sectionInset.right
let cellWidth = (totalWidth - CGFloat(columnCount - 1) * columnSpacing) / CGFloat(columnCount)
for idx in 0..<itemCount {
let indexPath = IndexPath(item: idx, section: 0)
//
let minCol = colHeight.firstIndex(of: colHeight.min()!)!
let x = sectionInset.left + CGFloat(minCol) * (cellWidth + columnSpacing)
let y = colHeight[minCol]
// ((UIScreen.width - 30.0) / 2.0) / (4.0 * 3.0)
let h = delegate?.collectionView(cv, layout: self, heightForItemAt: indexPath) ?? 100
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attr.frame = CGRect(x: x, y: y, width: cellWidth, height: h)
cache.append(attr)
//
colHeight[minCol] = y + h + interItemSpacing
}
contentHeight = colHeight.max()! + sectionInset.bottom
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
cache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
cache[indexPath.item]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
newBounds.width != collectionView?.bounds.width
}
}
// MARK: -
protocol WaterfallLayoutDelegate: AnyObject {
func collectionView(_ collectionView: UICollectionView,
layout: CLWaterfallLayout,
heightForItemAt indexPath: IndexPath) -> CGFloat
}

View File

@ -0,0 +1,133 @@
//
// RolesRootPageView.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/14.
//
import JXPagingView
import JXSegmentedView
import UIKit
import Combine
class RolesRootPageView: CLContainer {
let itemWidth: CGFloat = (UIScreen.width - 30.0) / 2.0
var jumpPublisher: AnyPublisher<JumpTarget, Never> { topView.jumpPublisher }
let data: [String] = [
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast",
"Once a prodigy, Lin Feng had his cultivation",
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy",
"Once a prodigy",
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast",
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy",
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy",
"Once a prodigy",
"Once a prodigy, Lin Feng had his cultivation shattered and was cast out Once a prodigy, Lin Feng had his cultivation shattered and was cast",
"Once a prodigy"
]
let remind: [String] = [
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]",
"[The Lsat Oracle of Kael]"
]
lazy var topView: CLTopHeaderView = {
let view = CLTopHeaderView()
return view
}()
lazy var collectionView: UICollectionView = {
let layout = CLWaterfallLayout()
layout.columnCount = 2
layout.delegate = self
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .clear
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(CLRoleCollectionCell.self, forCellWithReuseIdentifier: "CLRoleCollectionCell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(self.topView)
addSubview(collectionView)
topView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(UIDevice().navHeight)
}
collectionView.snp.makeConstraints { make in
make.bottom.left.right.equalToSuperview()
make.top.equalTo(topView.snp.bottom).offset(20)
}
}
}
extension RolesRootPageView: UICollectionViewDelegate, UICollectionViewDataSource, WaterfallLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CLRoleCollectionCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CLRoleCollectionCell", for: indexPath) as! CLRoleCollectionCell
cell.setupData(desc: data[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout: CLWaterfallLayout,
heightForItemAt indexPath: IndexPath) -> CGFloat {
let model = data[indexPath.item]
let coverH = itemWidth * (4.0 / 3.0)
//
let maxLines = 3
let font = UIFont.systemFont(ofSize: 12)
let lineHeight = font.lineHeight // 15 pt 17.4 pt
let maxHeight = lineHeight * CGFloat(maxLines)
// maxHeight
let textSize = model.boundingRect(
with: CGSize(width: itemWidth - 20, height: maxHeight), //
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: font],
context: nil).size
// 3 3 3
let textH = min(textSize.height, maxHeight)
let remindH = remind[indexPath.item].boundingRect(
with: CGSize(width: itemWidth - 20.0, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: UIFont.boldSystemFont(ofSize: 10)],
context: nil).height
return coverH + 10.0 + textH + 5.0 + remindH
}
}

View File

@ -63,7 +63,8 @@ extension TabBarController {
private func configViewControllers() {
let home = CLNavigationController(rootViewController: HomePageRootController())
let friend = CLNavigationController(rootViewController: FriendsRootHomeController())
let discover = CLNavigationController(rootViewController: DiscoverRootPageController())
// let discover = CLNavigationController(rootViewController: DiscoverRootPageController())
let discover = CLNavigationController(rootViewController: RolesRootPageController())
let me = CLNavigationController(rootViewController: MeRootPageController())
viewControllers = [home, friend, discover, me]

View File

@ -60,4 +60,26 @@ extension UIDevice {
return UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 > 0
}
}
//
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
let windowScene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first
return windowScene?.statusBarManager?.statusBarFrame.size.height ?? 0
} else {
return UIApplication.shared.statusBarFrame.size.height
}
}
//
var navBarHeight: CGFloat {
return UINavigationController().navigationBar.frame.height
}
// +
var navHeight: CGFloat {
return navBarHeight + statusBarHeight
}
}