- Feature: Markdown view integration complete (if issues arrise can fix it asap)

This commit is contained in:
mahee96
2025-02-28 05:09:37 +05:30
parent 2197161d55
commit c6703d66c1
5 changed files with 116 additions and 189 deletions

View File

@@ -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()
} }
} }
} }

View File

@@ -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"/>

View File

@@ -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,8 +108,6 @@ 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 boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
@@ -151,10 +142,6 @@ final class CollapsingTextView: UITextView
self.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
} }
self.shouldResetLayout = false
self.previousSize = self.bounds.size
}
} }
private extension CollapsingTextView private extension CollapsingTextView

View File

@@ -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

View File

@@ -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()
} }
@@ -80,6 +90,8 @@ final class CollapsingMarkdownView: UIView {
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
}
// Make sure this is called properly markdownParser.header.color = MarkdownManager.Color.header
private func updateToggleButtonTitle() { markdownParser.bold.color = MarkdownManager.Color.bold
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "") markdownParser.list.color = MarkdownManager.Color.bold
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))
if isCollapsed {
let buttonY = (textView.font?.lineHeight ?? 0) * CGFloat(maximumNumberOfLines - 1)
toggleButton.frame = CGRect( toggleButton.frame = CGRect(
x: bounds.width - buttonSize.width, x: bounds.width - buttonSize.width,
y: buttonY, y: textView.frame.maxY,
width: buttonSize.width, width: buttonSize.width,
height: textView.font?.lineHeight ?? 0 height: buttonHeight
) )
} 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 {