From c6703d66c163a505d51db29d353217a5cbf6da61 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Fri, 28 Feb 2025 05:09:37 +0530 Subject: [PATCH] - Feature: Markdown view integration complete (if issues arrise can fix it asap) --- .../App Detail/AppContentViewController.swift | 81 ++------- AltStore/Base.lproj/Main.storyboard | 2 +- AltStore/Components/CollapsingTextView.swift | 53 +++--- AltStore/My Apps/MyAppsViewController.swift | 1 + .../Views/UIKit/CollapsingMarkdownView.swift | 168 +++++++++--------- 5 files changed, 116 insertions(+), 189 deletions(-) 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 {