chat setting

This commit is contained in:
mh 2025-10-29 14:48:11 +08:00
parent 983b68bbba
commit 17f5eba096
74 changed files with 2174 additions and 6 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 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_exchange_mode@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_exchange_mode@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.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 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_text_mode@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_text_mode@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "role_voice@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "role_voice@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.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -395,9 +395,16 @@ extension SessionController {
// sessionNavigationView.upDownNoticeView.showUnlocked(string: "XY") // sessionNavigationView.upDownNoticeView.showUnlocked(string: "XY")
inputEntrance.inputTextView.resignFirstResponder() inputEntrance.inputTextView.resignFirstResponder()
let vc = ChatSettingListController() // let vc = ChatSettingListController()
vc.aiId = aiId // vc.aiId = aiId
navigationController?.pushViewController(vc, animated: true) // navigationController?.pushViewController(vc, animated: true)
UIView.animate(withDuration: 0.25) {
self.swipeBgView.alpha = 1.0
self.swipeView.snp.updateConstraints { make in
make.left.equalToSuperview().offset(UIScreen.width * 0.16)
}
self.view.layoutIfNeeded()
}
} }
@objc func tapLikeButton(){ @objc func tapLikeButton(){

View File

@ -12,6 +12,9 @@ class SessionController: CLBaseViewController {
var tableView: UITableView! var tableView: UITableView!
// var headView: SessionAIHeadView! // var headView: SessionAIHeadView!
var swipeView: ChatSettingSwipeView!
var swipeBgView: UIView!
// MARK: BottomViews // MARK: BottomViews
var bottomViewsStackV : InputStackView! var bottomViewsStackV : InputStackView!
var toolView: UIView! var toolView: UIView!
@ -197,6 +200,34 @@ extension SessionController {
view.bringSubviewToFront(sessionNavigationView) view.bringSubviewToFront(sessionNavigationView)
view.bringSubviewToFront(bottomViewsStackV) view.bringSubviewToFront(bottomViewsStackV)
swipeBgView = {
let bgView = UIView()
bgView.alpha = 0.0
bgView.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
view.addSubview(bgView)
bgView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(bgViewTap)))
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
return bgView
}()
swipeView = {
let v = ChatSettingSwipeView()
v.closeAction = { [weak self] in
self?.bgViewTap()
}
v.backgroundColor = .white
view.addSubview(v)
v.snp.makeConstraints { make in
make.left.equalToSuperview().inset(UIScreen.width)
make.top.bottom.equalToSuperview()
make.width.equalTo(UIScreen.width * 0.84)
}
return v
}()
} }
func setupUserInfo() { func setupUserInfo() {
@ -259,8 +290,15 @@ extension SessionController {
markReadAll() markReadAll()
} }
@objc func bgViewTap() {
UIView.animate(withDuration: 0.25) {
self.swipeBgView.alpha = 0.0
self.swipeView.snp.updateConstraints { make in
make.left.equalToSuperview().inset(UIScreen.width)
}
self.view.layoutIfNeeded()
}
}
} }

View File

@ -0,0 +1,132 @@
//
// ChatBackgroundCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
import SnapKit
struct BackgroundRow: RowModel {
let count: Int
var cellReuseID: String { "ChatBackgroundCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat {
// 使 UITableView.automaticDimension cell
return UITableView.automaticDimension
}
}
class ChatBackgroundCell: UITableViewCell, CellConfigurable {
private var collectionHeight: Constraint!
private var itemCount: Int = 6 // 6item
private var layout: UICollectionViewFlowLayout!
lazy var collectionView: UICollectionView = {
layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 10.0
layout.minimumInteritemSpacing = 10.0
layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isScrollEnabled = false
collectionView.backgroundColor = .clear
collectionView.register(ChatBgCollectionCell.self, forCellWithReuseIdentifier: "ChatBgCollectionCell")
return collectionView
}()
func configure(with row: RowModel) {
guard let row = row as? BackgroundRow else { return }
//
self.itemCount = row.count
// collectionView frame
DispatchQueue.main.async { [weak self] in
self?.updateCollectionViewLayout()
}
}
private var lastCalculatedWidth: CGFloat = 0
private func updateCollectionViewLayout() {
guard collectionView.frame.width > 0 else { return }
//
if abs(lastCalculatedWidth - collectionView.frame.width) < 1.0 {
return
}
lastCalculatedWidth = collectionView.frame.width
// sizeForItemAt
collectionView.reloadData()
//
collectionView.layoutIfNeeded()
//
let contentHeight = collectionView.collectionViewLayout.collectionViewContentSize.height
if contentHeight > 0 {
collectionHeight.update(offset: contentHeight)
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.width.equalToSuperview()
make.bottom.equalToSuperview().priority(999) //
collectionHeight = make.height.equalTo(100).priority(1000).constraint //
}
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
// collectionView frame
if collectionView.frame.width == 0 {
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width, height: 100)
updateCollectionViewLayout()
}
//
return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
}
}
extension ChatBackgroundCell: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ChatBgCollectionCell", for: indexPath) as! ChatBgCollectionCell
// indexPath cell
// cell.configure(with: data[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let availableWidth = collectionView.frame.width - layout.sectionInset.left - layout.sectionInset.right
let itemWidth = (availableWidth - 2 * layout.minimumInteritemSpacing) / 3.0
let itemHeight = itemWidth * (116.0 / 87)
return CGSize(width: itemWidth, height: itemHeight)
}
}

View File

@ -0,0 +1,84 @@
//
// ChatBgCollectionCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/28.
//
import UIKit
class ChatBgCollectionCell: UICollectionViewCell {
lazy var containView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hex: "#F3F4FF")
v.cornerRadius = 10.0
v.clipsToBounds = true
return v
}()
lazy var contentImgView: UIImageView = {
let imgView = UIImageView()
return imgView
}()
lazy var selectedImgView: UIImageView = {
let imgview = UIImageView(image: UIImage(named: "rolel_setting_selected"))
return imgview
}()
lazy var deleteImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "chat_setting_bg_delete"))
return imgView
}()
lazy var addImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "chat_setting_add_bg"))
return imgView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupData()
setupEvent()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
contentView.addSubview(containView)
containView.addSubview(contentImgView)
containView.addSubview(addImgView)
containView.addSubview(selectedImgView)
containView.addSubview(deleteImgView)
containView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentImgView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(2)
}
addImgView.snp.makeConstraints { make in
make.centerX.centerY.equalToSuperview()
}
selectedImgView.snp.makeConstraints { make in
make.right.bottom.equalToSuperview().inset(0)
}
deleteImgView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(7)
}
}
func setupData() {}
func setupEvent() {}
}

View File

@ -0,0 +1,118 @@
//
// ChatFontCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
// font
struct FontRow: RowModel {
let count: String
let icon: String
let title: String
var cellReuseID: String { "ChatFontCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
}
class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
lazy var iconImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_exchange_mode"))
return imgView
}()
lazy var titleLab: UILabel = {
let lab = UILabel()
lab.text = "XL-0826-32K"
lab.font = UIFont.boldSystemFont(ofSize: 14)
lab.textColor = UIColor(hex: "#666666")
return lab
}()
lazy var fontSub: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "role_setting_font_sub"), for: .normal)
btn.addTarget(self, action: #selector(fontSubTap), for: .touchUpInside)
return btn
}()
lazy var fontAdd: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "role_setting_font_add"), for: .normal)
btn.addTarget(self, action: #selector(fontAddTap), for: .touchUpInside)
return btn
}()
lazy var fontLab: UILabel = {
let lab = UILabel()
lab.text = "20"
lab.font = UIFont.systemFont(ofSize: 14)
lab.textColor = UIColor(hex: "#999999")
lab.textAlignment = .center
return lab
}()
func configure(with row: RowModel) {
guard let row = row as? FontRow else { return }
titleLab.text = row.title
iconImgView.image = UIImage(named: row.icon)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
containerView.addSubview(iconImgView)
containerView.addSubview(titleLab)
containerView.addSubview(fontSub)
containerView.addSubview(fontAdd)
containerView.addSubview(fontLab)
iconImgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(12)
make.width.height.equalTo(21)
}
titleLab.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(iconImgView.snp.right).offset(9)
make.right.equalTo(fontSub.snp.left).offset(-5)
}
fontAdd.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(20)
make.width.height.equalTo(40)
}
fontLab.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalTo(fontAdd.snp.left).offset(-10)
}
fontSub.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalTo(fontLab.snp.left).offset(-10)
make.width.height.equalTo(40)
}
}
@objc func fontSubTap() {
print("sub sub sub")
}
@objc func fontAddTap() {
print("add add add")
}
}

View File

@ -0,0 +1,204 @@
//
// ChatHistoryCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import Foundation
import SnapKit
struct HistoryRow: RowModel {
let time: String
let icon: String
let title: String
let itemCount: Int //
var cellReuseID: String { "ChatHistoryCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat {
// 使
let singleCellHeight = ChatHistoryContentCell.calculateHeight(for: tableWidth)
let deleteButtonHeight: CGFloat = 62 // DELETEfooter view
let totalHeight = CGFloat(itemCount) * singleCellHeight + deleteButtonHeight
return totalHeight
}
}
class ChatHistoryCell: UITableViewCell, CellConfigurable {
private var tableViewHeight: Constraint!
private var itemCount: Int = 3 // 3item
private var lastCalculatedHeight: CGFloat = 0
private var isLayoutConfigured = false
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 60 //
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .clear
tableView.showsVerticalScrollIndicator = false
tableView.register(ChatHistoryContentCell.self, forCellReuseIdentifier: "ChatHistoryContentCell")
tableView.contentInset = .zero
tableView.isScrollEnabled = false
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
func configure(with row: RowModel) {
guard let row = row as? HistoryRow else { return }
//
self.itemCount = row.itemCount
// tableView
tableView.reloadData()
//
if isLayoutConfigured {
updateTableViewLayout()
}
}
private func updateTableViewLayout() {
// tableView
tableView.layoutIfNeeded()
// contentSize
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
// tableView.contentSize.height footer view
let totalH = tableView.contentSize.height
//
guard abs(lastCalculatedHeight - totalH) > 1.0 else { return }
lastCalculatedHeight = totalH
//
tableViewHeight.update(offset: totalH)
// tableView
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// tableView
if let tableView = self.superview as? UITableView {
tableView.beginUpdates()
tableView.endUpdates()
} else {
// tableView
var currentView = self.superview
while currentView != nil {
if let tableView = currentView as? UITableView {
tableView.beginUpdates()
tableView.endUpdates()
break
}
currentView = currentView?.superview
}
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// Cell
guard contentView.bounds.width > 0 else { return }
// tableView
tableView.frame.size.width = contentView.bounds.width
//
if !isLayoutConfigured {
isLayoutConfigured = true
}
// tableView
updateTableViewLayout()
}
@objc func deleteBtnTap() {
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
contentView.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.width.equalToSuperview()
make.bottom.equalToSuperview().priority(.low) //
tableViewHeight = make.height.equalTo(100).priority(1000).constraint //
}
// tableView
tableView.setContentHuggingPriority(.required, for: .vertical)
tableView.setContentCompressionResistancePriority(.required, for: .vertical)
}
}
extension ChatHistoryCell: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatHistoryContentCell", for: indexPath) as! ChatHistoryContentCell
cell.selectionStyle = .none
// indexPath cell
// cell.configure(with: data[indexPath.item])
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFLOAT_MIN
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footer = UIView()
footer.backgroundColor = .clear
let deleteBtn = UIButton()
deleteBtn.cornerRadius = 25.0
deleteBtn.borderColor = UIColor(hex: "#FF3B30")
deleteBtn.borderWidth = 2.0
deleteBtn.setTitle("DELETE ", for: .normal)
deleteBtn.setTitleColor(UIColor(hex: "#FF3B30"), for: .normal)
deleteBtn.setImage(UIImage(named: "chat_setting_delete"), for: .normal)
deleteBtn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17.0)
deleteBtn.setUp(.default, padding: 5.0)
deleteBtn.addTarget(self, action: #selector(deleteBtnTap), for: .touchUpInside)
footer.addSubview(deleteBtn)
deleteBtn.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.bottom.equalToSuperview().inset(2)
make.left.right.equalToSuperview().inset(20)
make.height.equalTo(50)
}
return footer
}
}

View File

@ -0,0 +1,124 @@
//
// ChatHistoryContentCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
class ChatHistoryContentCell: UITableViewCell {
lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hex: "#F6F6F6")
view.cornerRadius = 15.0
return view
}()
lazy var iconImgView: UIImageView = {
let imgView = UIImageView()
imgView.cornerRadius = 12.5
imgView.backgroundColor = .blue
return imgView
}()
lazy var timeLab: UILabel = {
let lab = UILabel()
lab.text = "2025/09/26 17:30"
lab.textColor = UIColor.hexString("#999999", alpha: 0.6)
lab.font = UIFont.systemFont(ofSize: 12)
return lab
}()
lazy var contentLab: UILabel = {
let lab = UILabel()
lab.text = "The Boss Fell for Me: My Days Screwing Nuts at Foxconn"
lab.textColor = UIColor(hex: "#666666")
lab.font = UIFont.systemFont(ofSize: 14)
lab.numberOfLines = 2
return lab
}()
lazy var arrowImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_setting_go"))
return imgView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
contentView.addSubview(containerView)
containerView.addSubview(iconImgView)
containerView.addSubview(timeLab)
containerView.addSubview(contentLab)
containerView.addSubview(arrowImgView)
containerView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.bottom.equalToSuperview().inset(2.5)
}
iconImgView.snp.makeConstraints { make in
make.left.equalToSuperview().inset(12)
make.top.equalToSuperview().offset(10)
make.width.height.equalTo(25)
}
timeLab.snp.makeConstraints { make in
make.left.equalTo(iconImgView.snp.right).offset(8)
make.top.equalTo(iconImgView.snp.top)
make.right.equalTo(arrowImgView.snp.left).offset(-15)
}
contentLab.snp.makeConstraints { make in
make.left.equalTo(timeLab.snp.left)
make.right.equalTo(timeLab.snp.right).offset(-15)
make.top.equalTo(timeLab.snp.bottom).offset(6)
make.bottom.equalToSuperview().inset(10)
}
arrowImgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(15)
make.width.height.equalTo(15)
}
//
contentLab.setContentHuggingPriority(.required, for: .vertical)
contentLab.setContentCompressionResistancePriority(.required, for: .vertical)
}
// cell
static func calculateHeight(for width: CGFloat) -> CGFloat {
let containerWidth = width - 40 // 20inset
let iconHeight: CGFloat = 25
let iconTopMargin: CGFloat = 10
let timeToContentMargin: CGFloat = 6
let contentBottomMargin: CGFloat = 10
let cellTopBottomMargin: CGFloat = 5 // 2.5inset
//
let timeHeight: CGFloat = 12 // 12
// 2
let contentFont = UIFont.systemFont(ofSize: 14)
let contentWidth = containerWidth - 12 - 25 - 8 - 15 - 15 //
let contentHeight = contentFont.lineHeight * 2 // 2
let totalHeight = cellTopBottomMargin + iconTopMargin + max(iconHeight, timeHeight) + timeToContentMargin + contentHeight + contentBottomMargin + cellTopBottomMargin
return totalHeight
}
}

View File

@ -0,0 +1,91 @@
//
// ChatResponseTokenCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
// token
struct TokenRow: RowModel {
let count: String
var cellReuseID: String { "ChatResponseTokenCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
}
class ChatResponseTokenCell: ChatSettingBaseCell, CellConfigurable {
lazy var countLab: UILabel = {
let lab = UILabel()
lab.textColor = UIColor(hex: "#666666")
lab.font = UIFont.boldSystemFont(ofSize: 14)
lab.text = "0"
lab.textAlignment = .center
return lab
}()
lazy var subBtn: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "role_setting_sub"), for: .normal)
btn.addTarget(self, action: #selector(subBtnTap), for: .touchUpInside)
return btn
}()
lazy var addBtn: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "role_setting_add"), for: .normal)
btn.addTarget(self, action: #selector(addBtnTap), for: .touchUpInside)
return btn
}()
@objc func subBtnTap() {
}
@objc func addBtnTap() {
}
func configure(with row: RowModel) {
guard let row = row as? TokenRow else { return }
countLab.text = row.count
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
containerView.addSubview(subBtn)
containerView.addSubview(countLab)
containerView.addSubview(addBtn)
countLab.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(subBtn.snp.right).offset(5)
make.right.equalTo(addBtn.snp.left).offset(-5)
}
subBtn.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().inset(10)
make.width.equalTo(25)
make.height.equalTo(25)
}
addBtn.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(10)
make.width.equalTo(25)
make.height.equalTo(25)
}
}
}

View File

@ -0,0 +1,39 @@
//
// ChatSettingBaseCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
class ChatSettingBaseCell: UITableViewCell {
lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hex: "#F6F6F6")
view.cornerRadius = 15.0
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
contentView.addSubview(containerView)
containerView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.bottom.equalToSuperview().inset(2.5)
make.height.equalTo(45.0)
}
}
}

View File

@ -0,0 +1,120 @@
//
// ChatSwipeCell.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import UIKit
// icon title
struct ImageRow: RowModel {
let icon: String
let title: String
let showAvatar: Bool
let showArrow: Bool
let showSwitch: Bool
var cellReuseID: String { "ChatSwipeCell" }
func cellHeight(tableWidth: CGFloat) -> CGFloat { 50 }
}
class ChatSwipeCell: ChatSettingBaseCell, CellConfigurable {
lazy var iconImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_exchange_mode"))
return imgView
}()
lazy var titleLab: UILabel = {
let lab = UILabel()
lab.text = "XL-0826-32K"
lab.font = UIFont.boldSystemFont(ofSize: 14)
lab.textColor = UIColor(hex: "#666666")
return lab
}()
lazy var avatarView : UIImageView = {
let imgView = UIImageView()
imgView.cornerRadius = 10.5
imgView.backgroundColor = .blue
return imgView
}()
lazy var arrowImgView: UIImageView = {
let imgView = UIImageView(image: UIImage(named: "role_setting_go"))
return imgView
}()
lazy var switchControl: SevenSwitch = {
let con = SevenSwitch()
con.onTintColor = UIColor(hex: "#020025")
con.onThumbTintColor = UIColor(hex: "#00CC88")
con.inactiveColor = UIColor(hex: "#020025")
return con
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [avatarView, arrowImgView])
stackView.spacing = 5
stackView.distribution = .fill
stackView.alignment = .center
return stackView
}()
func configure(with row: RowModel) {
guard let row = row as? ImageRow else { return }
iconImgView.image = UIImage(named: row.icon)
titleLab.text = row.title
avatarView.isHidden = !row.showAvatar
arrowImgView.isHidden = !row.showArrow
switchControl.isHidden = !row.showSwitch
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureViews() {
containerView.addSubview(iconImgView)
containerView.addSubview(titleLab)
containerView.addSubview(stackView)
containerView.addSubview(switchControl)
iconImgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(12)
make.width.height.equalTo(21)
}
avatarView.snp.makeConstraints { make in
make.width.height.equalTo(21)
}
stackView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(15)
}
titleLab.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(iconImgView.snp.right).offset(9)
make.right.equalTo(stackView.snp.left).offset(-5)
}
switchControl.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().inset(15)
make.width.equalTo(45)
make.height.equalTo(23)
}
titleLab.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
titleLab.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
}
}

View File

@ -0,0 +1,19 @@
//
// ActionProtocol.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/27.
//
import Foundation
/// 1. ID
protocol RowModel {
var cellReuseID: String { get }
func cellHeight(tableWidth: CGFloat) -> CGFloat
}
/// 2. Cell
protocol CellConfigurable: AnyObject {
func configure(with row: RowModel)
}

View File

@ -0,0 +1,196 @@
//
// ChatSettingSwipeView.swift
// Visual_Novel_iOS
//
// Created by mh on 2025/10/24.
//
import UIKit
class ChatSettingSwipeView: CLContainer {
var closeAction: (()->Void)?
var sectionTitle: [String] = ["Switch Model", "Sound", "Maximum number of response tokens Maximum number of response tokens", "Appearance", "Background", "Historical Archives"]
var rows: [[RowModel]] = [
[ImageRow(icon: "role_exchange_mode", title: "XL-0826-32K", showAvatar: false, showArrow: true, showSwitch: false), ImageRow(icon: "role_text_mode", title: "Short Text Mode", showAvatar: false, showArrow: true, showSwitch: false)],
[ImageRow(icon: "role_voice", title: "Voice actor", showAvatar: true, showArrow: true, showSwitch: false), ImageRow(icon: "role_talk", title: "Play dialogue only", showAvatar: false, showArrow: false, showSwitch: true)],
[TokenRow(count: "2500")],
[FontRow(count: "20", icon: "role_font", title: "Font size"), ImageRow(icon: "role_chat_mode", title: "Chat Mode", showAvatar: false, showArrow: true, showSwitch: false), ImageRow(icon: "role_chat_buttle", title: "Chat buttle", showAvatar: false, showArrow: true, showSwitch: false)],
[BackgroundRow(count: 50)],
[HistoryRow(time: "", icon: "", title: "", itemCount: 30)]
]
lazy var titleLab: UILabel = {
let lab = UILabel()
lab.text = "Setting"
lab.font = UIFont.boldSystemFont(ofSize: 18)
lab.textColor = UIColor(hex: "#000000")
return lab
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "role_chat_close"), for: .normal)
btn.addTarget(self, action: #selector(closeBtnTap), for: .touchUpInside)
return btn
}()
lazy var newChatBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setBackgroundImage(UIImage(named: "role_new_chat"), for: .normal)
btn.addTarget(self, action: #selector(newChatBtnTap), for: .touchUpInside)
btn.setTitle("+ Start New Chat", for: .normal)
btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
btn.imageView?.contentMode = .scaleToFill
return btn
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 72
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .clear
tableView.showsVerticalScrollIndicator = false
tableView.tableFooterView = UIView()
tableView.register(ChatSwipeCell.self, forCellReuseIdentifier: "ChatSwipeCell")
tableView.register(ChatResponseTokenCell.self, forCellReuseIdentifier: "ChatResponseTokenCell")
tableView.register(ChatFontCell.self, forCellReuseIdentifier: "ChatFontCell")
tableView.register(ChatBackgroundCell.self, forCellReuseIdentifier: "ChatBackgroundCell")
tableView.register(ChatHistoryCell.self, forCellReuseIdentifier: "ChatHistoryCell")
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
navigationView = NavigationView()
navigationView?.backButton.isHidden = false
navigationView?.isHidden = true
addSubview(navigationView ?? UIView())
addSubview(titleLab)
addSubview(closeBtn)
addSubview(tableView)
addSubview(newChatBtn)
navigationView?.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.right.equalToSuperview()
make.height.equalTo(UIWindow.navBarTotalHeight)
}
titleLab.snp.makeConstraints { make in
make.left.equalToSuperview().offset(20)
if self.navigationView != nil {
make.centerY.equalTo(self.navigationView!.titleLabel.snp.centerY)
} else {
make.top.equalToSuperview().offset(UIDevice().statusBarHeight + 13.0)
}
make.right.equalTo(closeBtn.snp.left).offset(-10)
}
closeBtn.snp.makeConstraints { make in
make.centerY.equalTo(titleLab.snp.centerY)
make.right.equalToSuperview().inset(20)
make.width.height.equalTo(25)
}
newChatBtn.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.height.equalTo(50)
make.bottom.equalToSuperview().inset(UIDevice().safeBottom + 5.0)
}
tableView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(closeBtn.snp.bottom).offset(10)
make.bottom.equalTo(newChatBtn.snp.top).offset(-5)
}
// tableViewcontentInset
tableView.contentInsetAdjustmentBehavior = .never
tableView.automaticallyAdjustsScrollIndicatorInsets = false
}
}
extension ChatSettingSwipeView {
@objc func closeBtnTap() {
self.closeAction?()
}
@objc func newChatBtnTap() {
}
}
extension ChatSettingSwipeView: UITableViewDelegate, UITableViewDataSource {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// tableView
// contentInset
}
func numberOfSections(in tableView: UITableView) -> Int {
return rows.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rows[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = rows[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: row.cellReuseID, for: indexPath)
cell.selectionStyle = .none
(cell as? CellConfigurable)?.configure(with: row)
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = UIView()
let lab = UILabel()
lab.text = sectionTitle[section]
lab.textColor = UIColor(hex: "#333333")
lab.font = UIFont.boldSystemFont(ofSize: 16)
lab.numberOfLines = 0
header.addSubview(lab)
lab.snp.makeConstraints { make in
make.left.equalToSuperview().offset(20)
make.right.equalToSuperview()
make.top.equalToSuperview().inset(5)
make.bottom.equalToSuperview()
}
return header
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFLOAT_MIN
}
}

View File

@ -0,0 +1,543 @@
//
// SevenSwitch.swift
//
// Created by Benjamin Vogelzang on 6/20/14.
// Copyright (c) 2014 Ben Vogelzang. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import QuartzCore
@IBDesignable @objc open class SevenSwitch: UIControl {
// public
/*
* Set (without animation) whether the switch is on or off
*/
@IBInspectable open var on: Bool {
get {
return switchValue
}
set {
switchValue = newValue
self.setOn(newValue, animated: false)
}
}
/*
* Sets the background color that shows when the switch off and actively being touched.
* Defaults to light gray.
*/
@IBInspectable open var activeColor: UIColor = UIColor(red: 0.89, green: 0.89, blue: 0.89, alpha: 1) {
willSet {
if self.on && !self.isTracking {
backgroundView.backgroundColor = newValue
}
}
}
/*
* Sets the background color when the switch is off.
* Defaults to clear color.
*/
@IBInspectable open var inactiveColor: UIColor = UIColor.clear {
willSet {
if !self.on && !self.isTracking {
backgroundView.backgroundColor = newValue
}
}
}
/*
* Sets the background color that shows when the switch is on.
* Defaults to green.
*/
@IBInspectable open var onTintColor: UIColor = UIColor(red: 0.3, green: 0.85, blue: 0.39, alpha: 1) {
willSet {
if self.on && !self.isTracking {
backgroundView.backgroundColor = newValue
backgroundView.layer.borderColor = newValue.cgColor
}
}
}
/*
* Sets the border color that shows when the switch is off. Defaults to light gray.
*/
@IBInspectable open var bordersColor: UIColor = UIColor(red: 0.78, green: 0.78, blue: 0.8, alpha: 1) {
willSet {
if !self.on {
backgroundView.layer.borderColor = newValue.cgColor
}
}
}
/*
* Sets the knob color. Defaults to white.
*/
@IBInspectable open var thumbTintColor: UIColor = UIColor.white {
willSet {
if !userDidSpecifyOnThumbTintColor {
onThumbTintColor = newValue
}
if (!userDidSpecifyOnThumbTintColor || !self.on) && !self.isTracking {
thumbView.backgroundColor = newValue
}
}
}
/*
* Sets the knob color that shows when the switch is on. Defaults to white.
*/
@IBInspectable open var onThumbTintColor: UIColor = UIColor.white {
willSet {
userDidSpecifyOnThumbTintColor = true
if self.on && !self.isTracking {
thumbView.backgroundColor = newValue
}
}
}
/*
* Sets the shadow color of the knob. Defaults to gray.
*/
@IBInspectable open var shadowColor: UIColor = UIColor.gray {
willSet {
thumbView.layer.shadowColor = newValue.cgColor
}
}
/*
* Sets whether or not the switch edges are rounded.
* Set to NO to get a stylish square switch.
* Defaults to YES.
*/
@IBInspectable open var isRounded: Bool = true {
willSet {
if newValue {
backgroundView.layer.cornerRadius = self.frame.size.height * 0.5
thumbView.layer.cornerRadius = (self.frame.size.height * 0.5) - 1
}
else {
backgroundView.layer.cornerRadius = 2
thumbView.layer.cornerRadius = 2
}
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
}
}
/*
* Sets the image that shows on the switch thumb.
*/
@IBInspectable open var thumbImage: UIImage! {
willSet {
thumbImageView.image = newValue
}
}
/*
* Sets the image that shows when the switch is on.
* The image is centered in the area not covered by the knob.
* Make sure to size your images appropriately.
*/
@IBInspectable open var onImage: UIImage! {
willSet {
onImageView.image = newValue
}
}
/*
* Sets the image that shows when the switch is off.
* The image is centered in the area not covered by the knob.
* Make sure to size your images appropriately.
*/
@IBInspectable open var offImage: UIImage! {
willSet {
offImageView.image = newValue
}
}
/*
* Sets the text that shows when the switch is on.
* The text is centered in the area not covered by the knob.
*/
open var onLabel: UILabel!
/*
* Sets the text that shows when the switch is off.
* The text is centered in the area not covered by the knob.
*/
open var offLabel: UILabel!
// internal
internal var backgroundView: UIView!
internal var thumbView: UIView!
internal var onImageView: UIImageView!
internal var offImageView: UIImageView!
internal var thumbImageView: UIImageView!
// private
fileprivate var currentVisualValue: Bool = false
fileprivate var startTrackingValue: Bool = false
fileprivate var didChangeWhileTracking: Bool = false
fileprivate var isAnimating: Bool = false
fileprivate var userDidSpecifyOnThumbTintColor: Bool = false
fileprivate var switchValue: Bool = false
/*
* Initialization
*/
public convenience init() {
self.init(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override public init(frame: CGRect) {
let initialFrame = frame.isEmpty ? CGRect(x: 0, y: 0, width: 50, height: 30) : frame
super.init(frame: initialFrame)
self.setup()
}
/*
* Setup the individual elements of the switch and set default values
*/
fileprivate func setup() {
// background
self.backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height))
backgroundView.backgroundColor = UIColor.clear
backgroundView.layer.cornerRadius = self.frame.size.height * 0.5
backgroundView.layer.borderColor = self.bordersColor.cgColor
backgroundView.layer.borderWidth = 0.0
backgroundView.isUserInteractionEnabled = false
backgroundView.clipsToBounds = true
self.addSubview(backgroundView)
// on/off images
self.onImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width - self.frame.size.height, height: self.frame.size.height))
onImageView.alpha = 1.0
onImageView.contentMode = UIView.ContentMode.center
backgroundView.addSubview(onImageView)
self.offImageView = UIImageView(frame: CGRect(x: self.frame.size.height, y: 0, width: self.frame.size.width - self.frame.size.height, height: self.frame.size.height))
offImageView.alpha = 1.0
offImageView.contentMode = UIView.ContentMode.center
backgroundView.addSubview(offImageView)
// labels
self.onLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.frame.size.width - self.frame.size.height, height: self.frame.size.height))
onLabel.textAlignment = NSTextAlignment.center
onLabel.textColor = UIColor.lightGray
onLabel.font = UIFont.systemFont(ofSize: 12)
backgroundView.addSubview(onLabel)
self.offLabel = UILabel(frame: CGRect(x: self.frame.size.height, y: 0, width: self.frame.size.width - self.frame.size.height, height: self.frame.size.height))
offLabel.textAlignment = NSTextAlignment.center
offLabel.textColor = UIColor.lightGray
offLabel.font = UIFont.systemFont(ofSize: 12)
backgroundView.addSubview(offLabel)
// thumb
self.thumbView = UIView(frame: CGRect(x: 1, y: 1, width: self.frame.size.height - 2, height: self.frame.size.height - 2))
thumbView.backgroundColor = self.thumbTintColor
thumbView.layer.cornerRadius = (self.frame.size.height * 0.5) - 1
thumbView.layer.shadowColor = self.shadowColor.cgColor
thumbView.layer.shadowRadius = 2.0
thumbView.layer.shadowOpacity = 0.5
thumbView.layer.shadowOffset = CGSize(width: 0, height: 3)
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
thumbView.layer.masksToBounds = false
thumbView.isUserInteractionEnabled = false
self.addSubview(thumbView)
// thumb image
self.thumbImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: thumbView.frame.size.width, height: thumbView.frame.size.height))
thumbImageView.contentMode = UIView.ContentMode.center
thumbImageView.autoresizingMask = UIView.AutoresizingMask.flexibleWidth
thumbView.addSubview(thumbImageView)
self.on = false
}
override open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
super.beginTracking(touch, with: event)
startTrackingValue = self.on
didChangeWhileTracking = false
let activeKnobWidth = self.bounds.size.height - 2 + 5
isAnimating = true
UIView.animate(withDuration: 0.3, delay: 0.0, options: [UIView.AnimationOptions.curveEaseOut, UIView.AnimationOptions.beginFromCurrentState], animations: {
if self.on {
self.thumbView.frame = CGRect(x: self.bounds.size.width - (activeKnobWidth + 1), y: self.thumbView.frame.origin.y, width: activeKnobWidth, height: self.thumbView.frame.size.height)
self.backgroundView.backgroundColor = self.onTintColor
self.thumbView.backgroundColor = self.onThumbTintColor
}
else {
self.thumbView.frame = CGRect(x: self.thumbView.frame.origin.x, y: self.thumbView.frame.origin.y, width: activeKnobWidth, height: self.thumbView.frame.size.height)
self.backgroundView.backgroundColor = self.activeColor
self.thumbView.backgroundColor = self.thumbTintColor
}
}, completion: { finished in
self.isAnimating = false
})
let shadowAnim = CABasicAnimation(keyPath: "shadowPath")
shadowAnim.duration = 0.3
shadowAnim.fromValue = thumbView.layer.shadowPath
shadowAnim.toValue = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
thumbView.layer.add(shadowAnim, forKey: "shadowPath")
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
return true
}
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
super.continueTracking(touch, with: event)
// Get touch location
let lastPoint = touch.location(in: self)
// update the switch to the correct visuals depending on if
// they moved their touch to the right or left side of the switch
if lastPoint.x > self.bounds.size.width * 0.5 {
self.showOn(true)
if !startTrackingValue {
didChangeWhileTracking = true
}
}
else {
self.showOff(true)
if startTrackingValue {
didChangeWhileTracking = true
}
}
return true
}
override open func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
let previousValue = self.on
if didChangeWhileTracking {
self.setOn(currentVisualValue, animated: true)
}
else {
self.setOn(!self.on, animated: true)
}
if previousValue != self.on {
self.sendActions(for: UIControl.Event.valueChanged)
}
}
override open func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
// just animate back to the original value
if self.on {
self.showOn(true)
}
else {
self.showOff(true)
}
}
override open func layoutSubviews() {
super.layoutSubviews()
if !isAnimating {
let frame = self.frame
// background
backgroundView.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
backgroundView.layer.cornerRadius = self.isRounded ? frame.size.height * 0.5 : 2
// images
onImageView.frame = CGRect(x: 0, y: 0, width: frame.size.width - frame.size.height, height: frame.size.height)
offImageView.frame = CGRect(x: frame.size.height, y: 0, width: frame.size.width - frame.size.height, height: frame.size.height)
self.onLabel.frame = CGRect(x: 0, y: 0, width: frame.size.width - frame.size.height, height: frame.size.height)
self.offLabel.frame = CGRect(x: frame.size.height, y: 0, width: frame.size.width - frame.size.height, height: frame.size.height)
// thumb
let normalKnobWidth = frame.size.height - 2
if self.on {
thumbView.frame = CGRect(x: frame.size.width - (normalKnobWidth + 1), y: 1, width: frame.size.height - 2, height: normalKnobWidth)
thumbImageView.frame = CGRect(x: frame.size.width - normalKnobWidth, y: 0, width: normalKnobWidth, height: normalKnobWidth)
}
else {
thumbView.frame = CGRect(x: 1, y: 1, width: normalKnobWidth, height: normalKnobWidth)
thumbImageView.frame = CGRect(x: 0, y: 0, width: normalKnobWidth, height: normalKnobWidth)
}
thumbView.layer.cornerRadius = self.isRounded ? (frame.size.height * 0.5) - 1 : 2
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
}
}
/*
* Set the state of the switch to on or off, optionally animating the transition.
*/
open func setOn(_ isOn: Bool, animated: Bool) {
switchValue = isOn
if on {
self.showOn(animated)
}
else {
self.showOff(animated)
}
}
/*
* Detects whether the switch is on or off
*
* @return BOOL YES if switch is on. NO if switch is off
*/
open func isOn() -> Bool {
return self.on
}
/*
* update the looks of the switch to be in the on position
* optionally make it animated
*/
fileprivate func showOn(_ animated: Bool) {
let normalKnobWidth = self.bounds.size.height - 2
let activeKnobWidth = normalKnobWidth + 5
if animated {
isAnimating = true
UIView.animate(withDuration: 0.3, delay: 0.0, options: [UIView.AnimationOptions.curveEaseOut, UIView.AnimationOptions.beginFromCurrentState], animations: {
if self.isTracking {
self.thumbView.frame = CGRect(x: self.bounds.size.width - (activeKnobWidth + 1), y: self.thumbView.frame.origin.y, width: activeKnobWidth, height: self.thumbView.frame.size.height)
}
else {
self.thumbView.frame = CGRect(x: self.bounds.size.width - (normalKnobWidth + 1), y: self.thumbView.frame.origin.y, width: normalKnobWidth, height: self.thumbView.frame.size.height)
}
self.backgroundView.backgroundColor = self.onTintColor
self.backgroundView.layer.borderColor = self.onTintColor.cgColor
self.thumbView.backgroundColor = self.onThumbTintColor
self.onImageView.alpha = 1.0
self.offImageView.alpha = 0
self.onLabel.alpha = 1.0
self.offLabel.alpha = 0
}, completion: { finished in
self.isAnimating = false
})
let shadowAnim = CABasicAnimation(keyPath: "shadowPath")
shadowAnim.duration = 0.3
shadowAnim.fromValue = thumbView.layer.shadowPath
shadowAnim.toValue = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
thumbView.layer.add(shadowAnim, forKey: "shadowPath")
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
}
else {
if self.isTracking {
thumbView.frame = CGRect(x: self.bounds.size.width - (activeKnobWidth + 1), y: thumbView.frame.origin.y, width: activeKnobWidth, height: thumbView.frame.size.height)
}
else {
thumbView.frame = CGRect(x: self.bounds.size.width - (normalKnobWidth + 1), y: thumbView.frame.origin.y, width: normalKnobWidth, height: thumbView.frame.size.height)
}
backgroundView.backgroundColor = self.onTintColor
backgroundView.layer.borderColor = self.onTintColor.cgColor
thumbView.backgroundColor = self.onThumbTintColor
onImageView.alpha = 1.0
offImageView.alpha = 0
onLabel.alpha = 1.0
offLabel.alpha = 0
}
currentVisualValue = true
}
/*
* update the looks of the switch to be in the off position
* optionally make it animated
*/
fileprivate func showOff(_ animated: Bool) {
let normalKnobWidth = self.bounds.size.height - 2
let activeKnobWidth = normalKnobWidth + 5
if animated {
isAnimating = true
UIView.animate(withDuration: 0.3, delay: 0.0, options: [UIView.AnimationOptions.curveEaseOut, UIView.AnimationOptions.beginFromCurrentState], animations: {
if self.isTracking {
self.thumbView.frame = CGRect(x: 1, y: self.thumbView.frame.origin.y, width: activeKnobWidth, height: self.thumbView.frame.size.height);
self.backgroundView.backgroundColor = self.activeColor
}
else {
self.thumbView.frame = CGRect(x: 1, y: self.thumbView.frame.origin.y, width: normalKnobWidth, height: self.thumbView.frame.size.height);
self.backgroundView.backgroundColor = self.inactiveColor
}
self.backgroundView.layer.borderColor = self.bordersColor.cgColor
self.thumbView.backgroundColor = self.thumbTintColor
self.onImageView.alpha = 0
self.offImageView.alpha = 1.0
self.onLabel.alpha = 0
self.offLabel.alpha = 1.0
}, completion: { finished in
self.isAnimating = false
})
let shadowAnim = CABasicAnimation(keyPath: "shadowPath")
shadowAnim.duration = 0.3
shadowAnim.fromValue = thumbView.layer.shadowPath
shadowAnim.toValue = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
thumbView.layer.add(shadowAnim, forKey: "shadowPath")
thumbView.layer.shadowPath = UIBezierPath(roundedRect: thumbView.bounds, cornerRadius: thumbView.layer.cornerRadius).cgPath
}
else {
if (self.isTracking) {
thumbView.frame = CGRect(x: 1, y: thumbView.frame.origin.y, width: activeKnobWidth, height: thumbView.frame.size.height)
backgroundView.backgroundColor = self.activeColor
}
else {
thumbView.frame = CGRect(x: 1, y: thumbView.frame.origin.y, width: normalKnobWidth, height: thumbView.frame.size.height)
backgroundView.backgroundColor = self.inactiveColor
}
backgroundView.layer.borderColor = self.bordersColor.cgColor
thumbView.backgroundColor = self.thumbTintColor
onImageView.alpha = 0
offImageView.alpha = 1.0
onLabel.alpha = 0
offLabel.alpha = 1.0
}
currentVisualValue = false
}
}

View File

@ -49,7 +49,7 @@ extension UIDevice {
} }
var hasNotch: Bool { var hasNotch: Bool {
if #available(iOS 15.0, *) { if #available(iOS 13.0, *) {
// Use UIWindowScene.windows for iOS 15 and later // Use UIWindowScene.windows for iOS 15 and later
let windowScene = UIApplication.shared.connectedScenes let windowScene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene } .compactMap { $0 as? UIWindowScene }
@ -61,6 +61,19 @@ extension UIDevice {
} }
} }
var safeBottom: CGFloat {
if #available(iOS 13.0, *) {
// Use UIWindowScene.windows for iOS 15 and later
let windowScene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first
return windowScene?.windows.first?.safeAreaInsets.bottom ?? 0.0
} else {
// Fallback for iOS versions before 15.0
return UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0.0
}
}
// //
var statusBarHeight: CGFloat { var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {