mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Merge pull request #897 from SideStore/markdown-for-update-description
Feature: Render in-app update description as (full) markdown
This commit is contained in:
@@ -87,6 +87,9 @@
|
|||||||
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
|
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
|
||||||
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
|
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
|
||||||
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
|
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
|
||||||
|
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */; };
|
||||||
|
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B645FE2D70C1AD00125819 /* MarkdownKit */; };
|
||||||
|
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B646002D70C23E00125819 /* MarkdownKit */; };
|
||||||
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
|
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
|
||||||
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
|
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
|
||||||
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
|
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
|
||||||
@@ -686,6 +689,7 @@
|
|||||||
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
|
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
|
||||||
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
|
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
|
||||||
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
|
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
|
||||||
|
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsingMarkdownView.swift; sourceTree = "<group>"; };
|
||||||
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
||||||
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
|
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
|
||||||
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
|
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
|
||||||
@@ -1137,6 +1141,7 @@
|
|||||||
files = (
|
files = (
|
||||||
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */,
|
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */,
|
||||||
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */,
|
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */,
|
||||||
|
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */,
|
||||||
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */,
|
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */,
|
||||||
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */,
|
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */,
|
||||||
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */,
|
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */,
|
||||||
@@ -1144,6 +1149,7 @@
|
|||||||
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
|
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
|
||||||
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
|
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
|
||||||
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
|
||||||
|
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */,
|
||||||
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1346,6 +1352,22 @@
|
|||||||
path = database;
|
path = database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A8B645F82D70C0DD00125819 /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8B645FA2D70C0F600125819 /* UIKit */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8B645FA2D70C0F600125819 /* UIKit */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */,
|
||||||
|
);
|
||||||
|
path = UIKit;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
A8C38C1C2D2068D100E83DBD /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1427,6 +1449,7 @@
|
|||||||
A8F66C072D04C025009689E6 /* SideStore */ = {
|
A8F66C072D04C025009689E6 /* SideStore */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A8B645F82D70C0DD00125819 /* Views */,
|
||||||
A8E2DB352D6850A9009E5D31 /* Tests */,
|
A8E2DB352D6850A9009E5D31 /* Tests */,
|
||||||
A8F66C5C2D04D433009689E6 /* minimuxer */,
|
A8F66C5C2D04D433009689E6 /* minimuxer */,
|
||||||
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
|
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
|
||||||
@@ -2682,6 +2705,7 @@
|
|||||||
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
|
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
|
||||||
D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
|
D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
|
||||||
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
|
||||||
|
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -3292,6 +3316,7 @@
|
|||||||
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
|
||||||
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
|
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
|
||||||
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
|
||||||
|
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */,
|
||||||
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
|
||||||
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
|
||||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||||
@@ -4259,6 +4284,14 @@
|
|||||||
minimumVersion = 0.4.0;
|
minimumVersion = 0.4.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.7.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
|
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin.git";
|
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin.git";
|
||||||
@@ -4283,6 +4316,16 @@
|
|||||||
package = A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
|
package = A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
|
||||||
productName = SemanticVersion;
|
productName = SemanticVersion;
|
||||||
};
|
};
|
||||||
|
A8B645FE2D70C1AD00125819 /* MarkdownKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
|
||||||
|
productName = MarkdownKit;
|
||||||
|
};
|
||||||
|
A8B646002D70C23E00125819 /* MarkdownKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
|
||||||
|
productName = MarkdownKit;
|
||||||
|
};
|
||||||
A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */ = {
|
A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = "AltSign-Static";
|
productName = "AltSign-Static";
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ 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 versionDescriptionTextView: CollapsingTextView!
|
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
|
||||||
|
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||||
|
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
|
||||||
@IBOutlet private var versionLabel: UILabel!
|
@IBOutlet private var versionLabel: UILabel!
|
||||||
@IBOutlet private var versionDateLabel: UILabel!
|
@IBOutlet private var versionDateLabel: UILabel!
|
||||||
@IBOutlet private var sizeLabel: UILabel!
|
@IBOutlet private var sizeLabel: UILabel!
|
||||||
@@ -55,35 +57,32 @@ 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()
|
override func viewDidLoad() {
|
||||||
{
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
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
|
|
||||||
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
||||||
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
|
||||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
|
||||||
}
|
} else {
|
||||||
else
|
self.versionDescriptionTextView.text = "nil"
|
||||||
{
|
|
||||||
self.versionDescriptionTextView.text = nil
|
|
||||||
self.versionLabel.text = nil
|
self.versionLabel.text = nil
|
||||||
self.versionDateLabel.text = nil
|
self.versionDateLabel.text = nil
|
||||||
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.descriptionTextView.maximumNumberOfLines = 5
|
self.descriptionTextView.maximumNumberOfLines = 5
|
||||||
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.versionDescriptionTextView.maximumNumberOfLines = 5
|
||||||
|
|
||||||
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews()
|
override func viewDidLayoutSubviews()
|
||||||
@@ -162,8 +161,12 @@ private extension AppContentViewController
|
|||||||
|
|
||||||
switch sender
|
switch sender
|
||||||
{
|
{
|
||||||
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
case self.descriptionTextView.toggleButton:
|
||||||
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||||
|
|
||||||
|
case self.versionDescriptionTextView.toggleButton:
|
||||||
|
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||||
|
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"/>
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
|
<rect key="frame" x="20" y="59.5" 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,7 +235,8 @@ 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.text = latestSupportedVersion.localizedDescription
|
cell.versionDescriptionTextView.maximumNumberOfLines = 2
|
||||||
|
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
|
||||||
|
|
||||||
cell.bannerView.iconImageView.image = nil
|
cell.bannerView.iconImageView.image = nil
|
||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
@@ -281,12 +282,9 @@ private extension MyAppsViewController
|
|||||||
cell.mode = .collapsed
|
cell.mode = .collapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
|
cell.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
cell.setNeedsLayout()
|
cell.setNeedsLayout()
|
||||||
|
|
||||||
// Below lines are necessary to avoid "more" button layout issues.
|
|
||||||
cell.versionDescriptionTextView.setNeedsLayout()
|
|
||||||
cell.layoutIfNeeded()
|
cell.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
||||||
@@ -724,22 +722,29 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
|
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
|
||||||
|
|
||||||
|
// Toggle the state
|
||||||
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
|
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
|
||||||
{
|
{
|
||||||
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
|
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
|
||||||
|
// Set collapsed mode on the cell
|
||||||
cell?.mode = .collapsed
|
cell?.mode = .collapsed
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
|
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
|
||||||
|
// Set expanded mode on the cell
|
||||||
cell?.mode = .expanded
|
cell?.mode = .expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear cached size so it's recalculated
|
||||||
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
|
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
|
||||||
|
|
||||||
self.collectionView.performBatchUpdates({
|
// Animate the change smoothly with a duration
|
||||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
UIView.animate(withDuration: 0.25) {
|
||||||
}, completion: nil)
|
self.collectionView.performBatchUpdates({
|
||||||
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
}, completion: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func refreshApp(_ sender: UIButton)
|
@IBAction func refreshApp(_ sender: UIButton)
|
||||||
|
|||||||
@@ -21,12 +21,19 @@ extension UpdateCollectionViewCell
|
|||||||
{
|
{
|
||||||
var mode: Mode = .expanded {
|
var mode: Mode = .expanded {
|
||||||
didSet {
|
didSet {
|
||||||
self.update()
|
switch self.mode {
|
||||||
|
case .collapsed:
|
||||||
|
self.versionDescriptionTextView.isCollapsed = true
|
||||||
|
case .expanded:
|
||||||
|
self.versionDescriptionTextView.isCollapsed = false
|
||||||
|
}
|
||||||
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBOutlet var bannerView: AppBannerView!
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
|
||||||
|
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
|
||||||
|
|
||||||
@IBOutlet private var blurView: UIVisualEffectView!
|
@IBOutlet private var blurView: UIVisualEffectView!
|
||||||
|
|
||||||
@@ -85,16 +92,16 @@ extension UpdateCollectionViewCell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
|
||||||
{
|
// {
|
||||||
// Ensure cell is laid out so it will report correct size.
|
// // Ensure cell is laid out so it will report correct size.
|
||||||
self.versionDescriptionTextView.setNeedsLayout()
|
// self.versionDescriptionTextView.setNeedsLayout()
|
||||||
self.versionDescriptionTextView.layoutIfNeeded()
|
// self.versionDescriptionTextView.layoutIfNeeded()
|
||||||
|
//
|
||||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||||
|
//
|
||||||
return size
|
// return size
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UpdateCollectionViewCell
|
private extension UpdateCollectionViewCell
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
|
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
|
||||||
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
|
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
|
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
<systemColor name="secondaryLabelColor">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
301
SideStore/Views/UIKit/CollapsingMarkdownView.swift
Normal file
301
SideStore/Views/UIKit/CollapsingMarkdownView.swift
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
//
|
||||||
|
// CollapsingMarkdownView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 27/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MarkdownKit
|
||||||
|
|
||||||
|
struct MarkdownManager
|
||||||
|
{
|
||||||
|
struct Fonts{
|
||||||
|
static let body: UIFont = .systemFont(ofSize: UIFont.systemFontSize)
|
||||||
|
// static let body: UIFont = .systemFont(ofSize: UIFont.labelFontSize)
|
||||||
|
|
||||||
|
static let header: UIFont = .boldSystemFont(ofSize: 14)
|
||||||
|
static let list: UIFont = .systemFont(ofSize: 14)
|
||||||
|
static let bold: UIFont = .boldSystemFont(ofSize: 14)
|
||||||
|
static let italic: 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 {
|
||||||
|
[
|
||||||
|
.header,
|
||||||
|
.list,
|
||||||
|
.quote,
|
||||||
|
.code,
|
||||||
|
.link,
|
||||||
|
.bold,
|
||||||
|
.italic,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
var markdownParser: MarkdownParser {
|
||||||
|
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.updateCollapsedState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maximumNumberOfLines = 3 {
|
||||||
|
didSet {
|
||||||
|
self.checkIfNeedsCollapsing()
|
||||||
|
self.updateCollapsedState()
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String = "" {
|
||||||
|
didSet {
|
||||||
|
self.updateMarkdownContent()
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineSpacing: Double = 2 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = font.lineHeight
|
||||||
|
|
||||||
|
// Safely calculate actual line count
|
||||||
|
actualLineCount = max(1, Int(ceil(textSize.height / lineHeight)))
|
||||||
|
|
||||||
|
// 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
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
// Update the button title
|
||||||
|
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
|
||||||
|
toggleButton.setTitle(title, for: .normal)
|
||||||
|
|
||||||
|
// Set max lines based on collapsed state
|
||||||
|
if isCollapsed && needsCollapsing {
|
||||||
|
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
|
||||||
|
} else {
|
||||||
|
textView.textContainer.maximumNumberOfLines = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button is only visible if content needs collapsing
|
||||||
|
toggleButton.isHidden = !needsCollapsing
|
||||||
|
|
||||||
|
// Force layout updates
|
||||||
|
textView.layoutIfNeeded()
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize() {
|
||||||
|
// Configure text view
|
||||||
|
textView.isEditable = false
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
|
textView.textContainer.lineBreakMode = .byTruncatingTail
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
|
||||||
|
// Make textView selectable to enable link interactions
|
||||||
|
textView.isSelectable = true
|
||||||
|
textView.delegate = self
|
||||||
|
|
||||||
|
// Important: This prevents selection handles from appearing
|
||||||
|
textView.dataDetectorTypes = .link
|
||||||
|
|
||||||
|
// Configure markdown parser
|
||||||
|
configureMarkdownParser()
|
||||||
|
|
||||||
|
// Add subviews
|
||||||
|
addSubview(textView)
|
||||||
|
|
||||||
|
// Configure toggle button
|
||||||
|
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
|
||||||
|
addSubview(toggleButton)
|
||||||
|
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureMarkdownParser() {
|
||||||
|
// Configure markdown parser with desired settings
|
||||||
|
markdownParser.enabledElements = MarkdownManager.enabledElements
|
||||||
|
|
||||||
|
// You can also customize the styling if needed
|
||||||
|
markdownParser.header.font = MarkdownManager.Fonts.header
|
||||||
|
markdownParser.list.font = MarkdownManager.Fonts.list
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
// Calculate button height (for spacing)
|
||||||
|
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
isCollapsed.toggle()
|
||||||
|
didToggleCollapse?()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Markdown Processing
|
||||||
|
private func updateMarkdownContent() {
|
||||||
|
let attributedString = markdownParser.parse(text)
|
||||||
|
|
||||||
|
// Apply line spacing
|
||||||
|
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.lineSpacing = lineSpacing
|
||||||
|
|
||||||
|
mutableAttributedString.addAttribute(
|
||||||
|
.paragraphStyle,
|
||||||
|
value: paragraphStyle,
|
||||||
|
range: NSRange(location: 0, length: mutableAttributedString.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
textView.attributedText = mutableAttributedString
|
||||||
|
|
||||||
|
// Check if content needs collapsing after setting text
|
||||||
|
checkIfNeedsCollapsing()
|
||||||
|
updateCollapsedState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CollapsingMarkdownView: UITextViewDelegate {
|
||||||
|
// This enables tapping on links while preventing text selection
|
||||||
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
|
// Open the URL using UIApplication
|
||||||
|
UIApplication.shared.open(URL)
|
||||||
|
return false // Return false to prevent the default behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// This prevents text selection
|
||||||
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||||
|
textView.selectedTextRange = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user