mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
- Feature: Markdown view integration complete (if issues arrise can fix it asap)
This commit is contained in:
@@ -43,7 +43,8 @@ final class AppContentViewController: UITableViewController
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
@IBOutlet private var subtitleLabel: UILabel!
|
@IBOutlet private var subtitleLabel: UILabel!
|
||||||
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
// @IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||||
|
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
|
||||||
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||||
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
||||||
@IBOutlet private var versionLabel: UILabel!
|
@IBOutlet private var versionLabel: UILabel!
|
||||||
@@ -55,37 +56,6 @@ final class AppContentViewController: UITableViewController
|
|||||||
|
|
||||||
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
|
||||||
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
|
||||||
//
|
|
||||||
// override func viewDidLoad()
|
|
||||||
// {
|
|
||||||
// super.viewDidLoad()
|
|
||||||
//
|
|
||||||
// self.tableView.contentInset.bottom = 20
|
|
||||||
//
|
|
||||||
// self.subtitleLabel.text = self.app.subtitle
|
|
||||||
// self.descriptionTextView.text = self.app.localizedDescription
|
|
||||||
//
|
|
||||||
// if let version = self.app.latestAvailableVersion
|
|
||||||
// {
|
|
||||||
// self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
|
||||||
// self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
|
||||||
// self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
|
||||||
// self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// self.versionDescriptionTextView.text = "nil"
|
|
||||||
// self.versionLabel.text = nil
|
|
||||||
// self.versionDateLabel.text = nil
|
|
||||||
// self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// self.descriptionTextView.maximumNumberOfLines = 5
|
|
||||||
// self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
|
||||||
//
|
|
||||||
// self.versionDescriptionTextView.maximumNumberOfLines = 3
|
|
||||||
// self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
|
||||||
// }
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@@ -93,7 +63,8 @@ final class AppContentViewController: UITableViewController
|
|||||||
self.tableView.contentInset.bottom = 20
|
self.tableView.contentInset.bottom = 20
|
||||||
|
|
||||||
self.subtitleLabel.text = self.app.subtitle
|
self.subtitleLabel.text = self.app.subtitle
|
||||||
self.descriptionTextView.text = self.app.localizedDescription
|
let desc = self.app.localizedDescription
|
||||||
|
self.descriptionTextView.text = desc
|
||||||
|
|
||||||
if let version = self.app.latestAvailableVersion {
|
if let version = self.app.latestAvailableVersion {
|
||||||
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
|
||||||
@@ -107,20 +78,11 @@ final class AppContentViewController: UITableViewController
|
|||||||
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set maximum number of lines (no extra target-action needed)
|
|
||||||
self.descriptionTextView.maximumNumberOfLines = 5
|
self.descriptionTextView.maximumNumberOfLines = 5
|
||||||
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
self.versionDescriptionTextView.maximumNumberOfLines = 5
|
||||||
|
|
||||||
// Instead of adding another target for toggle, set the didToggleCollapse callback:
|
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
self.versionDescriptionTextView.didToggleCollapse = { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
UIView.animate(withDuration: 0.25) {
|
|
||||||
self.tableView.beginUpdates()
|
|
||||||
self.tableView.endUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews()
|
override func viewDidLayoutSubviews()
|
||||||
@@ -199,37 +161,18 @@ private extension AppContentViewController
|
|||||||
|
|
||||||
switch sender
|
switch sender
|
||||||
{
|
{
|
||||||
case self.descriptionTextView.moreButton:
|
case self.descriptionTextView.toggleButton:
|
||||||
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||||
// Toggle the state for the text view
|
|
||||||
self.descriptionTextView.isCollapsed.toggle()
|
|
||||||
|
|
||||||
case self.versionDescriptionTextView.toggleButton:
|
case self.versionDescriptionTextView.toggleButton:
|
||||||
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||||
// Toggle the state for the markdown view
|
|
||||||
self.versionDescriptionTextView.isCollapsed.toggle()
|
|
||||||
|
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First apply the new state to the views
|
// Disable animations to prevent some potentially strange ones.
|
||||||
switch Row.allCases[indexPath.row] {
|
UIView.performWithoutAnimation {
|
||||||
case .description:
|
self.tableView.reloadRows(at: [indexPath], with: .none)
|
||||||
self.descriptionTextView.setNeedsLayout()
|
|
||||||
self.descriptionTextView.layoutIfNeeded()
|
|
||||||
|
|
||||||
case .versionDescription:
|
|
||||||
self.versionDescriptionTextView.setNeedsLayout()
|
|
||||||
self.versionDescriptionTextView.layoutIfNeeded()
|
|
||||||
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then reload the row with animation
|
|
||||||
UIView.animate(withDuration: 0.25) {
|
|
||||||
// Force table view to recalculate height
|
|
||||||
self.tableView.beginUpdates()
|
|
||||||
self.tableView.endUpdates()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,7 +287,7 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="20" y="20" width="335" height="34"/>
|
<rect key="frame" x="20" y="20" width="335" height="34"/>
|
||||||
<color key="backgroundColor" name="Background"/>
|
<color key="backgroundColor" name="Background"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
|
|||||||
@@ -13,21 +13,18 @@ final class CollapsingTextView: UITextView
|
|||||||
var isCollapsed = true {
|
var isCollapsed = true {
|
||||||
didSet {
|
didSet {
|
||||||
guard self.isCollapsed != oldValue else { return }
|
guard self.isCollapsed != oldValue else { return }
|
||||||
self.shouldResetLayout = true
|
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maximumNumberOfLines = 2 {
|
var maximumNumberOfLines = 2 {
|
||||||
didSet {
|
didSet {
|
||||||
self.shouldResetLayout = true
|
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lineSpacing: Double = 2 {
|
var lineSpacing: Double = 2 {
|
||||||
didSet {
|
didSet {
|
||||||
self.shouldResetLayout = true
|
|
||||||
|
|
||||||
if #available(iOS 16, *)
|
if #available(iOS 16, *)
|
||||||
{
|
{
|
||||||
@@ -42,7 +39,6 @@ final class CollapsingTextView: UITextView
|
|||||||
|
|
||||||
override var text: String! {
|
override var text: String! {
|
||||||
didSet {
|
didSet {
|
||||||
self.shouldResetLayout = true
|
|
||||||
|
|
||||||
guard #available(iOS 16, *) else { return }
|
guard #available(iOS 16, *) else { return }
|
||||||
self.updateText()
|
self.updateText()
|
||||||
@@ -51,9 +47,6 @@ final class CollapsingTextView: UITextView
|
|||||||
|
|
||||||
let moreButton = UIButton(type: .system)
|
let moreButton = UIButton(type: .system)
|
||||||
|
|
||||||
private var shouldResetLayout: Bool = false
|
|
||||||
private var previousSize: CGSize?
|
|
||||||
|
|
||||||
override init(frame: CGRect, textContainer: NSTextContainer?)
|
override init(frame: CGRect, textContainer: NSTextContainer?)
|
||||||
{
|
{
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
@@ -115,45 +108,39 @@ final class CollapsingTextView: UITextView
|
|||||||
height: font.lineHeight)
|
height: font.lineHeight)
|
||||||
self.moreButton.frame = moreButtonFrame
|
self.moreButton.frame = moreButtonFrame
|
||||||
|
|
||||||
if self.shouldResetLayout || self.previousSize != self.bounds.size
|
if self.isCollapsed
|
||||||
{
|
{
|
||||||
if self.isCollapsed
|
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||||
|
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
|
||||||
|
|
||||||
|
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
||||||
{
|
{
|
||||||
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
||||||
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
|
|
||||||
|
|
||||||
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
|
var exclusionFrame = moreButtonFrame
|
||||||
{
|
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
||||||
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
|
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
||||||
|
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
||||||
var exclusionFrame = moreButtonFrame
|
|
||||||
exclusionFrame.origin.y += self.moreButton.bounds.midY
|
self.moreButton.isHidden = false
|
||||||
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
|
|
||||||
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
|
||||||
|
|
||||||
self.moreButton.isHidden = false
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
|
|
||||||
self.textContainer.exclusionPaths = []
|
|
||||||
|
|
||||||
self.moreButton.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.textContainer.maximumNumberOfLines = 0
|
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
|
||||||
self.textContainer.exclusionPaths = []
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
self.moreButton.isHidden = true
|
self.moreButton.isHidden = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.textContainer.maximumNumberOfLines = 0
|
||||||
|
self.textContainer.exclusionPaths = []
|
||||||
|
|
||||||
self.invalidateIntrinsicContentSize()
|
self.moreButton.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
self.shouldResetLayout = false
|
self.invalidateIntrinsicContentSize()
|
||||||
self.previousSize = self.bounds.size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ private extension MyAppsViewController
|
|||||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
cell.tintColor = app.tintColor ?? .altPrimary
|
cell.tintColor = app.tintColor ?? .altPrimary
|
||||||
|
cell.versionDescriptionTextView.maximumNumberOfLines = 2
|
||||||
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
|
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
|
||||||
|
|
||||||
cell.bannerView.iconImageView.image = nil
|
cell.bannerView.iconImageView.image = nil
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ struct MarkdownManager
|
|||||||
static let quote: UIFont = .italicSystemFont(ofSize: 14)
|
static let quote: UIFont = .italicSystemFont(ofSize: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Color{
|
||||||
|
static let header = UIColor { traitCollection in
|
||||||
|
traitCollection.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
|
||||||
|
}
|
||||||
|
static let bold = UIColor { traitCollection in
|
||||||
|
traitCollection.userInterfaceStyle == .dark ? UIColor.lightText : UIColor.darkText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static var enabledElements: MarkdownParser.EnabledElements {
|
static var enabledElements: MarkdownParser.EnabledElements {
|
||||||
[
|
[
|
||||||
.header,
|
.header,
|
||||||
@@ -36,26 +45,27 @@ struct MarkdownManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var markdownParser: MarkdownParser {
|
var markdownParser: MarkdownParser {
|
||||||
MarkdownParser(font: Self.Fonts.body)
|
MarkdownParser(
|
||||||
|
font: Self.Fonts.body,
|
||||||
|
color: Self.Color.bold
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CollapsingMarkdownView: UIView {
|
final class CollapsingMarkdownView: UIView {
|
||||||
/// Called when the collapse state toggles.
|
/// Called when the collapse state toggles.
|
||||||
var didToggleCollapse: (() -> Void)?
|
var didToggleCollapse: (() -> Void)?
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
var isCollapsed = true {
|
var isCollapsed = true {
|
||||||
didSet {
|
didSet {
|
||||||
guard self.isCollapsed != oldValue else { return }
|
guard self.isCollapsed != oldValue else { return }
|
||||||
self.updateToggleButtonTitle()
|
|
||||||
self.updateCollapsedState()
|
self.updateCollapsedState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maximumNumberOfLines = 3 {
|
var maximumNumberOfLines = 3 {
|
||||||
didSet {
|
didSet {
|
||||||
|
self.checkIfNeedsCollapsing()
|
||||||
self.updateCollapsedState()
|
self.updateCollapsedState()
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
@@ -75,11 +85,13 @@ final class CollapsingMarkdownView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let toggleButton = UIButton(type: .system)
|
let toggleButton = UIButton(type: .system)
|
||||||
|
|
||||||
private let textView = UITextView()
|
private let textView = UITextView()
|
||||||
private let markdownParser = MarkdownManager().markdownParser
|
private let markdownParser = MarkdownManager().markdownParser
|
||||||
|
|
||||||
private var previousSize: CGSize?
|
private var previousSize: CGSize?
|
||||||
|
private var actualLineCount: Int = 0
|
||||||
|
private var needsCollapsing = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
@@ -98,54 +110,46 @@ final class CollapsingMarkdownView: UIView {
|
|||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var needsCollapsing = false
|
|
||||||
private func checkIfNeedsCollapsing() {
|
private func checkIfNeedsCollapsing() {
|
||||||
|
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
|
||||||
|
needsCollapsing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the number of lines in the text
|
||||||
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||||
let lineHeight = textView.font?.lineHeight ?? 0
|
let lineHeight = font.lineHeight
|
||||||
let actualLines = textSize.height / lineHeight
|
|
||||||
|
|
||||||
needsCollapsing = actualLines > CGFloat(maximumNumberOfLines)
|
// Safely calculate actual line count
|
||||||
|
actualLineCount = max(1, Int(ceil(textSize.height / lineHeight)))
|
||||||
|
|
||||||
// Hide toggle button if no collapsing needed
|
// Only needs collapsing if actual lines exceed the maximum
|
||||||
|
needsCollapsing = actualLineCount > maximumNumberOfLines
|
||||||
|
|
||||||
|
// Update button visibility
|
||||||
toggleButton.isHidden = !needsCollapsing
|
toggleButton.isHidden = !needsCollapsing
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCollapsedState() {
|
private func updateCollapsedState() {
|
||||||
// Disable animations for this update to prevent gradual rearrangement
|
// Disable animations for this update
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
// Update the button title
|
// Update the button title
|
||||||
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
|
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
|
||||||
toggleButton.setTitle(title, for: .normal)
|
toggleButton.setTitle(title, for: .normal)
|
||||||
|
|
||||||
// Make sure toggle button is only visible when needed
|
// Set max lines based on collapsed state
|
||||||
toggleButton.isHidden = !needsCollapsing
|
|
||||||
|
|
||||||
// Update text view constraints
|
|
||||||
if isCollapsed && needsCollapsing {
|
if isCollapsed && needsCollapsing {
|
||||||
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
|
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
|
||||||
|
|
||||||
// Create exclusion path for button
|
|
||||||
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
|
||||||
let buttonY = (textView.font?.lineHeight ?? 0) * CGFloat(maximumNumberOfLines - 1)
|
|
||||||
|
|
||||||
let exclusionFrame = CGRect(
|
|
||||||
x: bounds.width - buttonSize.width - 5, // Add some padding
|
|
||||||
y: buttonY,
|
|
||||||
width: buttonSize.width + 10, // Add padding around button
|
|
||||||
height: (textView.font?.lineHeight ?? 0) + 5
|
|
||||||
)
|
|
||||||
|
|
||||||
textView.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
|
|
||||||
} else {
|
} else {
|
||||||
textView.textContainer.maximumNumberOfLines = 0
|
textView.textContainer.maximumNumberOfLines = 0
|
||||||
textView.textContainer.exclusionPaths = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force an immediate layout update
|
// Button is only visible if content needs collapsing
|
||||||
|
toggleButton.isHidden = !needsCollapsing
|
||||||
|
|
||||||
|
// Force layout updates
|
||||||
textView.layoutIfNeeded()
|
textView.layoutIfNeeded()
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
|
|
||||||
// Invalidate intrinsic content size to ensure proper sizing
|
|
||||||
self.invalidateIntrinsicContentSize()
|
self.invalidateIntrinsicContentSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,13 +176,10 @@ final class CollapsingMarkdownView: UIView {
|
|||||||
// Add subviews
|
// Add subviews
|
||||||
addSubview(textView)
|
addSubview(textView)
|
||||||
|
|
||||||
// Configure toggle button instead of more button
|
// Configure toggle button
|
||||||
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
|
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
|
||||||
addSubview(toggleButton)
|
addSubview(toggleButton)
|
||||||
|
|
||||||
// Update the button title based on current state
|
|
||||||
updateToggleButtonTitle()
|
|
||||||
|
|
||||||
setNeedsLayout()
|
setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,78 +193,73 @@ final class CollapsingMarkdownView: UIView {
|
|||||||
markdownParser.bold.font = MarkdownManager.Fonts.bold
|
markdownParser.bold.font = MarkdownManager.Fonts.bold
|
||||||
markdownParser.italic.font = MarkdownManager.Fonts.italic
|
markdownParser.italic.font = MarkdownManager.Fonts.italic
|
||||||
markdownParser.quote.font = MarkdownManager.Fonts.quote
|
markdownParser.quote.font = MarkdownManager.Fonts.quote
|
||||||
|
|
||||||
|
markdownParser.header.color = MarkdownManager.Color.header
|
||||||
|
markdownParser.bold.color = MarkdownManager.Color.bold
|
||||||
|
markdownParser.list.color = MarkdownManager.Color.bold
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure this is called properly
|
|
||||||
private func updateToggleButtonTitle() {
|
|
||||||
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
|
|
||||||
toggleButton.setTitle(title, for: .normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Layout
|
// MARK: - Layout
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
textView.frame = bounds
|
// Calculate button height (for spacing)
|
||||||
|
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
|
||||||
|
|
||||||
// Check if content needs collapsing when layout changes
|
// Set textView frame to leave space for button
|
||||||
|
textView.frame = CGRect(
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height - buttonHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if layout changed
|
||||||
if previousSize?.width != bounds.width {
|
if previousSize?.width != bounds.width {
|
||||||
checkIfNeedsCollapsing()
|
checkIfNeedsCollapsing()
|
||||||
|
updateCollapsedState()
|
||||||
previousSize = bounds.size
|
previousSize = bounds.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only position toggle button if it's needed
|
// Position toggle button at bottom right
|
||||||
if !toggleButton.isHidden {
|
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||||
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
toggleButton.frame = CGRect(
|
||||||
|
x: bounds.width - buttonSize.width,
|
||||||
if isCollapsed {
|
y: textView.frame.maxY,
|
||||||
let buttonY = (textView.font?.lineHeight ?? 0) * CGFloat(maximumNumberOfLines - 1)
|
width: buttonSize.width,
|
||||||
toggleButton.frame = CGRect(
|
height: buttonHeight
|
||||||
x: bounds.width - buttonSize.width,
|
)
|
||||||
y: buttonY,
|
|
||||||
width: buttonSize.width,
|
|
||||||
height: textView.font?.lineHeight ?? 0
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Position at the end of content when expanded
|
|
||||||
let textHeight = textView.sizeThatFits(bounds.size).height
|
|
||||||
let lineHeight = textView.font?.lineHeight ?? 0
|
|
||||||
toggleButton.frame = CGRect(
|
|
||||||
x: bounds.width - buttonSize.width,
|
|
||||||
y: textHeight - lineHeight,
|
|
||||||
width: buttonSize.width,
|
|
||||||
height: lineHeight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func toggleCollapsed(_ sender: UIButton) {
|
@objc private func toggleCollapsed(_ sender: UIButton) {
|
||||||
// Toggle the state instantly
|
|
||||||
isCollapsed.toggle()
|
isCollapsed.toggle()
|
||||||
|
|
||||||
// Update the UI without animation
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
updateToggleButtonTitle()
|
|
||||||
updateCollapsedState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify any observer that a toggle occurred
|
|
||||||
didToggleCollapse?()
|
didToggleCollapse?()
|
||||||
}
|
}
|
||||||
|
|
||||||
override var intrinsicContentSize: CGSize {
|
override var intrinsicContentSize: CGSize {
|
||||||
if isCollapsed {
|
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
|
||||||
guard let font = textView.font else { return super.intrinsicContentSize }
|
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
||||||
let height = font.lineHeight * CGFloat(maximumNumberOfLines) + lineSpacing * CGFloat(maximumNumberOfLines - 1)
|
}
|
||||||
return CGSize(width: UIView.noIntrinsicMetric, height: height)
|
|
||||||
|
let lineHeight = font.lineHeight
|
||||||
|
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
|
||||||
|
|
||||||
|
// Always add button height to reserve space for it
|
||||||
|
if isCollapsed && needsCollapsing {
|
||||||
|
// When collapsed and needs collapsing, use maximumNumberOfLines
|
||||||
|
let collapsedHeight = lineHeight * CGFloat(maximumNumberOfLines) +
|
||||||
|
lineSpacing * CGFloat(max(0, maximumNumberOfLines - 1))
|
||||||
|
return CGSize(width: UIView.noIntrinsicMetric, height: collapsedHeight + buttonHeight)
|
||||||
|
} else if !needsCollapsing {
|
||||||
|
// Text is shorter than max lines - use actual text height
|
||||||
|
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||||
|
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
|
||||||
} else {
|
} else {
|
||||||
// When expanded, use the full content size of the text view
|
// When expanded and needs collapsing, use full text height plus button
|
||||||
let size = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
|
||||||
return CGSize(width: UIView.noIntrinsicMetric, height: size.height)
|
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,8 +282,8 @@ final class CollapsingMarkdownView: UIView {
|
|||||||
|
|
||||||
// Check if content needs collapsing after setting text
|
// Check if content needs collapsing after setting text
|
||||||
checkIfNeedsCollapsing()
|
checkIfNeedsCollapsing()
|
||||||
|
updateCollapsedState()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CollapsingMarkdownView: UITextViewDelegate {
|
extension CollapsingMarkdownView: UITextViewDelegate {
|
||||||
|
|||||||
Reference in New Issue
Block a user