Draw the Cells

This commit is contained in:
砍砍
2024-12-13 13:51:51 +08:00
parent 2fee181633
commit 8333f00aec
10 changed files with 347 additions and 10 deletions

View File

@@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "networkimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/NetworkImage",
"state" : {
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
"version" : "6.0.1"
}
},
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark",
"state" : {
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
"version" : "0.5.0"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
"state" : {
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
"version" : "2.4.1"
}
}
],
"version" : 2
}

View File

@@ -7,13 +7,18 @@ let package = Package(
name: "Intelligents",
defaultLocalization: "en",
platforms: [
.iOS(.v14),
.macCatalyst(.v14),
.iOS(.v15),
.macCatalyst(.v15),
],
products: [
.library(name: "Intelligents", targets: ["Intelligents"]),
],
dependencies: [
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.1"),
],
targets: [
.target(name: "Intelligents"),
.target(name: "Intelligents", dependencies: [
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
]),
]
)

View File

@@ -7,19 +7,56 @@
import UIKit
private let initialInsetValue: CGFloat = 24
extension ChatTableView {
class BaseCell: UITableViewCell {
var inset: UIEdgeInsets = .zero {
didSet { setNeedsLayout() }
var inset: UIEdgeInsets { // available for overrides
.init(
top: initialInsetValue / 2,
left: initialInsetValue,
bottom: initialInsetValue / 2,
right: initialInsetValue
)
}
let containerView = UIView()
let roundedBackgroundView = UIView()
var viewModel: AnyObject? {
didSet { update(via: viewModel) }
}
var isBackgroundColorActivated = false {
didSet {
roundedBackgroundView.backgroundColor = isBackgroundColorActivated
? .systemGray.withAlphaComponent(0.25)
: .clear
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(roundedBackgroundView)
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
[ // inset half of the container view
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left / 2),
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right / 2),
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top / 2),
roundedBackgroundView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom / 2),
].forEach { $0.isActive = true }
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
[
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset.left),
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset.right),
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset.top),
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset.bottom),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
@@ -27,13 +64,13 @@ extension ChatTableView {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
containerView.frame = contentView.bounds.inset(by: inset)
override func prepareForReuse() {
super.prepareForReuse()
viewModel = nil
}
func update(via _: AnyObject?) {
assertionFailure() // "should be override"
func update(via object: AnyObject?) {
_ = object
}
}
}

View File

@@ -0,0 +1,121 @@
//
// ChatTableView+ChatCell.swift
// Intelligents
//
// Created by on 2024/11/18.
//
import MarkdownUI
import UIKit
extension ChatTableView {
class ChatCell: BaseCell {
let avatarView = CircleImageView()
let titleLabel = UILabel()
let markdownContainer = UIView()
var markdownView: UIView?
var removableConstraints: [NSLayoutConstraint] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let spacingElement: CGFloat = 12
let avatarSize: CGFloat = 24
containerView.addSubview(avatarView)
avatarView.translatesAutoresizingMaskIntoConstraints = false
[
avatarView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
avatarView.topAnchor.constraint(equalTo: containerView.topAnchor),
avatarView.widthAnchor.constraint(equalToConstant: avatarSize),
avatarView.heightAnchor.constraint(equalToConstant: avatarSize),
].forEach { $0.isActive = true }
titleLabel.font = .systemFont(ofSize: UIFont.labelFontSize, weight: .bold)
containerView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
[
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: spacingElement),
titleLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
].forEach { $0.isActive = true }
containerView.addSubview(markdownContainer)
markdownContainer.translatesAutoresizingMaskIntoConstraints = false
[
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: avatarView.bottomAnchor, constant: spacingElement),
markdownContainer.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: spacingElement),
markdownContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
markdownContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
markdownContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
].forEach { $0.isActive = true }
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func update(via object: AnyObject?) {
super.update(via: object)
guard let viewModel = object as? ViewModel else {
return
}
switch viewModel.participant {
case .system:
avatarView.image = UIImage(systemName: "gearshape.fill")
titleLabel.text = "System".localized()
case .assistant:
avatarView.image = UIImage(named: "spark", in: .module, with: .none)
titleLabel.text = "AFFiNE AI".localized()
case .user:
avatarView.image = UIImage(systemName: "person.fill")
titleLabel.text = "You".localized()
}
removableConstraints.forEach { $0.isActive = false }
if let markdownView { markdownView.removeFromSuperview() }
markdownContainer.subviews.forEach { $0.removeFromSuperview() }
let hostingView: UIView = UIHostingView(
rootView: Markdown(.init(viewModel.markdownDocument))
)
defer { markdownView = hostingView }
markdownContainer.addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
[
hostingView.topAnchor.constraint(equalTo: markdownContainer.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: markdownContainer.leadingAnchor),
hostingView.trailingAnchor.constraint(lessThanOrEqualTo: markdownContainer.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: markdownContainer.bottomAnchor),
].forEach {
$0.isActive = true
removableConstraints.append($0)
}
}
}
}
extension ChatTableView.ChatCell {
class ViewModel {
let participant: Participant
let markdownDocument: String
init(participant: Participant, markdownDocument: String) {
self.participant = participant
self.markdownDocument = markdownDocument
}
}
}
extension ChatTableView.ChatCell.ViewModel {
enum Participant {
case user
case assistant
case system
}
}

View File

@@ -11,6 +11,7 @@ extension ChatTableView {
struct DataElement {
enum CellType: String, CaseIterable {
case base
case chat
}
let type: CellType
@@ -28,6 +29,8 @@ extension ChatTableView.DataElement.CellType {
switch self {
case .base:
ChatTableView.BaseCell.self
case .chat:
ChatTableView.ChatCell.self
}
}

View File

@@ -39,6 +39,51 @@ class ChatTableView: UIView {
foot.heightAnchor.constraint(equalToConstant: 200).isActive = true
foot.widthAnchor.constraint(equalToConstant: 200).isActive = true
tableView.tableFooterView = foot
DispatchQueue.main.async {
self.dataSource = [
.init(type: .chat, object: ChatCell.ViewModel(
participant: .system,
markdownDocument: "Welcome to Intelligents"
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .user,
markdownDocument: "Please summarize this article for me"
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .assistant,
markdownDocument: ###"""
**Activation Code Usage Limits**
A single activation code can be used on multiple devices.
**Note:** A single activation code is intended for use on a reasonable number of devices by one user.
Excessive activation requests may result in the activation code being banned. Any bans are subject to manual review and are operated by staff.
`The limit is up to 5 devices per year or 10 activation requests within the same period.`
"""###
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .user,
markdownDocument: ###"""
**Download Axchange from the App Store**
You can download Axchange from the App Store:
- [https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944](https://apps.apple.com/app/axchange-adb-file-transfer/id6737504944)
The version downloaded this way does not require activation to use.
"""###
)),
.init(type: .chat, object: ChatCell.ViewModel(
participant: .assistant,
markdownDocument: "GOOD"
)),
]
self.tableView.reloadData()
}
}
@available(*, unavailable)

View File

@@ -20,3 +20,12 @@
/* No comment provided by engineer. */
"Summarize this article for me..." = "Summarize this article for me...";
/* No comment provided by engineer. */
"System" = "System";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE AI";
/* No comment provided by engineer. */
"You" = "You";

View File

@@ -20,3 +20,12 @@
/* No comment provided by engineer. */
"Summarize this article for me..." = "请为我总结这份文档...";
/* No comment provided by engineer. */
"System" = "系统";
/* No comment provided by engineer. */
"AFFiNE AI" = "AFFiNE AI";
/* No comment provided by engineer. */
"You" = "你";

View File

@@ -0,0 +1,29 @@
//
// CircleImageView.swift
// Intelligents
//
// Created by on 2024/12/13.
//
import UIKit
class CircleImageView: UIImageView {
init() {
super.init(frame: .zero)
contentMode = .scaleAspectFill
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
layer.cornerRadius = (bounds.width + bounds.height) / 2 / 2
layer.masksToBounds = true
}
}

View File

@@ -0,0 +1,47 @@
//
// UIHostingView.swift
// Intelligents
//
// Created by on 2024/12/13.
//
import SwiftUI
import UIKit
class UIHostingView<Content: View>: UIView {
private let hostingViewController: UIHostingController<Content>
var rootView: Content {
get { hostingViewController.rootView }
set { hostingViewController.rootView = newValue }
}
init(rootView: Content) {
hostingViewController = UIHostingController(rootView: rootView)
super.init(frame: .zero)
hostingViewController.view?.translatesAutoresizingMaskIntoConstraints = false
addSubview(hostingViewController.view)
if let view = hostingViewController.view {
view.backgroundColor = .clear
view.isOpaque = false
addSubview(view)
let constraints = [
view.topAnchor.constraint(equalTo: topAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.leftAnchor.constraint(equalTo: leftAnchor),
view.rightAnchor.constraint(equalTo: rightAnchor),
]
NSLayoutConstraint.activate(constraints)
}
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
hostingViewController.sizeThatFits(in: size)
}
}