角色模块UI
22
Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 832 B |
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_check_icon.imageset/header_check_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_discord_icon.imageset/header_discord_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.6 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_money_icon.imageset/header_money_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.9 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Base/header_not_subscriber_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 789 B |
After Width: | Height: | Size: 1.5 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_search_icon.imageset/header_search_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.2 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Base/header_subscriber_icon.imageset/header_subscriber_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.5 KiB |
|
@ -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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@2x.png
vendored
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_bottom_bg.imageset/role_bottom_bg@3x.png
vendored
Normal file
After Width: | Height: | Size: 76 KiB |
22
Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 869 B |
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_close_icon.imageset/role_close_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 794 B |
After Width: | Height: | Size: 1.8 KiB |
|
@ -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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 876 B |
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_open_icon.imageset/role_open_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.0 KiB |
|
@ -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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@2x.png
vendored
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_top_bg.imageset/role_top_bg@3x.png
vendored
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -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
|
||||
}
|
||||
}
|
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@2x.png
vendored
Normal file
After Width: | Height: | Size: 675 B |
BIN
Visual_Novel_iOS/Assets.xcassets/Role/role_video.imageset/role_video@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// CLContainerVC.swift
|
||||
// Visual_Novel_iOS
|
||||
//
|
||||
// Created by mh on 2025/10/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// tabbar的四个rootvc都用共同的header 做一个包容的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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import UIKit
|
||||
|
||||
class CLTabRootController<Container: UIView>: CLViewController<Container>{
|
||||
private var bgTopIv: UIImageView!
|
||||
var bgTopIv: UIImageView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
|
|
@ -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) // 关联值可以带参数
|
||||
}
|
||||
|
||||
|
||||
/// tabbar上rootvc顶部头像等公共区域
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|