diff --git a/AltStore/App Detail/AppContentViewController.swift b/AltStore/App Detail/AppContentViewController.swift
index 10fd0993..8ba1a195 100644
--- a/AltStore/App Detail/AppContentViewController.swift
+++ b/AltStore/App Detail/AppContentViewController.swift
@@ -43,7 +43,8 @@ final class AppContentViewController: UITableViewController
}()
@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: CollapsingMarkdownView!
@IBOutlet private var versionLabel: UILabel!
@@ -55,37 +56,6 @@ final class AppContentViewController: UITableViewController
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
@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() {
super.viewDidLoad()
@@ -93,7 +63,8 @@ final class AppContentViewController: UITableViewController
self.tableView.contentInset.bottom = 20
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 {
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
@@ -107,20 +78,11 @@ final class AppContentViewController: UITableViewController
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
}
- // Set maximum number of lines (no extra target-action needed)
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.moreButton.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()
- }
- }
+ self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
+ self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
}
override func viewDidLayoutSubviews()
@@ -199,37 +161,18 @@ private extension AppContentViewController
switch sender
{
- case self.descriptionTextView.moreButton:
+ case self.descriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
- // Toggle the state for the text view
- self.descriptionTextView.isCollapsed.toggle()
case self.versionDescriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
- // Toggle the state for the markdown view
- self.versionDescriptionTextView.isCollapsed.toggle()
-
+
default: return
}
- // First apply the new state to the views
- switch Row.allCases[indexPath.row] {
- case .description:
- 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()
+ // Disable animations to prevent some potentially strange ones.
+ UIView.performWithoutAnimation {
+ self.tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard
index 5962d35f..d33cf363 100644
--- a/AltStore/Base.lproj/Main.storyboard
+++ b/AltStore/Base.lproj/Main.storyboard
@@ -287,7 +287,7 @@
-
+
diff --git a/AltStore/Components/CollapsingTextView.swift b/AltStore/Components/CollapsingTextView.swift
index cebc8cac..6c74e3d7 100644
--- a/AltStore/Components/CollapsingTextView.swift
+++ b/AltStore/Components/CollapsingTextView.swift
@@ -13,21 +13,18 @@ final class CollapsingTextView: UITextView
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
- self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
- self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
- self.shouldResetLayout = true
if #available(iOS 16, *)
{
@@ -42,7 +39,6 @@ final class CollapsingTextView: UITextView
override var text: String! {
didSet {
- self.shouldResetLayout = true
guard #available(iOS 16, *) else { return }
self.updateText()
@@ -51,9 +47,6 @@ final class CollapsingTextView: UITextView
let moreButton = UIButton(type: .system)
- private var shouldResetLayout: Bool = false
- private var previousSize: CGSize?
-
override init(frame: CGRect, textContainer: NSTextContainer?)
{
super.init(frame: frame, textContainer: textContainer)
@@ -115,45 +108,39 @@ final class CollapsingTextView: UITextView
height: font.lineHeight)
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)
- let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
+ self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
- if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
- {
- self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
-
- var exclusionFrame = moreButtonFrame
- exclusionFrame.origin.y += self.moreButton.bounds.midY
- 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
- }
+ var exclusionFrame = moreButtonFrame
+ exclusionFrame.origin.y += self.moreButton.bounds.midY
+ 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
+ self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
+ }
+ else
+ {
+ self.textContainer.maximumNumberOfLines = 0
+ self.textContainer.exclusionPaths = []
- self.invalidateIntrinsicContentSize()
+ self.moreButton.isHidden = true
}
- self.shouldResetLayout = false
- self.previousSize = self.bounds.size
+ self.invalidateIntrinsicContentSize()
}
}
diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index a7dee21c..b4a0851b 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -235,6 +235,7 @@ private extension MyAppsViewController
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
+ cell.versionDescriptionTextView.maximumNumberOfLines = 2
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
cell.bannerView.iconImageView.image = nil
diff --git a/SideStore/Views/UIKit/CollapsingMarkdownView.swift b/SideStore/Views/UIKit/CollapsingMarkdownView.swift
index 133f092c..2c4a2a9f 100644
--- a/SideStore/Views/UIKit/CollapsingMarkdownView.swift
+++ b/SideStore/Views/UIKit/CollapsingMarkdownView.swift
@@ -23,6 +23,15 @@ struct MarkdownManager
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 {
[
.header,
@@ -36,26 +45,27 @@ struct MarkdownManager
}
var markdownParser: MarkdownParser {
- MarkdownParser(font: Self.Fonts.body)
+ MarkdownParser(
+ font: Self.Fonts.body,
+ color: Self.Color.bold
+ )
}
}
-
final class CollapsingMarkdownView: UIView {
/// Called when the collapse state toggles.
var didToggleCollapse: (() -> Void)?
-
// MARK: - Properties
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
- self.updateToggleButtonTitle()
self.updateCollapsedState()
}
}
var maximumNumberOfLines = 3 {
didSet {
+ self.checkIfNeedsCollapsing()
self.updateCollapsedState()
self.setNeedsLayout()
}
@@ -75,11 +85,13 @@ final class CollapsingMarkdownView: UIView {
}
let toggleButton = UIButton(type: .system)
-
+
private let textView = UITextView()
private let markdownParser = MarkdownManager().markdownParser
private var previousSize: CGSize?
+ private var actualLineCount: Int = 0
+ private var needsCollapsing = false
// MARK: - Initialization
@@ -98,54 +110,46 @@ final class CollapsingMarkdownView: UIView {
initialize()
}
- private var needsCollapsing = false
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 lineHeight = textView.font?.lineHeight ?? 0
- let actualLines = textSize.height / lineHeight
+ let lineHeight = font.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
}
private func updateCollapsedState() {
- // Disable animations for this update to prevent gradual rearrangement
+ // Disable animations for this update
UIView.performWithoutAnimation {
// Update the button title
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
toggleButton.setTitle(title, for: .normal)
- // Make sure toggle button is only visible when needed
- toggleButton.isHidden = !needsCollapsing
-
- // Update text view constraints
+ // Set max lines based on collapsed state
if isCollapsed && needsCollapsing {
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 {
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()
self.layoutIfNeeded()
-
- // Invalidate intrinsic content size to ensure proper sizing
self.invalidateIntrinsicContentSize()
}
}
@@ -172,13 +176,10 @@ final class CollapsingMarkdownView: UIView {
// Add subviews
addSubview(textView)
- // Configure toggle button instead of more button
+ // Configure toggle button
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
addSubview(toggleButton)
- // Update the button title based on current state
- updateToggleButtonTitle()
-
setNeedsLayout()
}
@@ -192,78 +193,73 @@ final class CollapsingMarkdownView: UIView {
markdownParser.bold.font = MarkdownManager.Fonts.bold
markdownParser.italic.font = MarkdownManager.Fonts.italic
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
override func layoutSubviews() {
super.layoutSubviews()
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 {
checkIfNeedsCollapsing()
+ updateCollapsedState()
previousSize = bounds.size
}
- // Only position toggle button if it's needed
- if !toggleButton.isHidden {
- let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
-
- if isCollapsed {
- let buttonY = (textView.font?.lineHeight ?? 0) * CGFloat(maximumNumberOfLines - 1)
- toggleButton.frame = CGRect(
- 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
- )
- }
- }
+ // Position toggle button at bottom right
+ let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
+ toggleButton.frame = CGRect(
+ x: bounds.width - buttonSize.width,
+ y: textView.frame.maxY,
+ width: buttonSize.width,
+ height: buttonHeight
+ )
}
}
@objc private func toggleCollapsed(_ sender: UIButton) {
- // Toggle the state instantly
isCollapsed.toggle()
-
- // Update the UI without animation
- UIView.performWithoutAnimation {
- updateToggleButtonTitle()
- updateCollapsedState()
- }
-
- // Notify any observer that a toggle occurred
didToggleCollapse?()
}
override var intrinsicContentSize: CGSize {
- if isCollapsed {
- guard let font = textView.font else { return super.intrinsicContentSize }
- let height = font.lineHeight * CGFloat(maximumNumberOfLines) + lineSpacing * CGFloat(maximumNumberOfLines - 1)
- return CGSize(width: UIView.noIntrinsicMetric, height: height)
+ guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
+ return CGSize(width: UIView.noIntrinsicMetric, height: 0)
+ }
+
+ 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 {
- // When expanded, use the full content size of the text view
- let size = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
- return CGSize(width: UIView.noIntrinsicMetric, height: size.height)
+ // When expanded and needs collapsing, use full text height plus button
+ let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
+ 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
checkIfNeedsCollapsing()
+ updateCollapsedState()
}
-
}
extension CollapsingMarkdownView: UITextViewDelegate {