Skip to content

Commit c5d3ae9

Browse files
committed
chore: upgrade minimum iOS version from 15.0 to 18.0
- Update IPHONEOS_DEPLOYMENT_TARGET to 18.0 in project.pbxproj - Re-enable iOS 16+ Regex API in MarkdownRenderer and RenderActor - Remove iOS 15 compatibility workarounds and fallback code - Change @available annotations from iOS 15.0 to iOS 16.0 - Fix MarkdownRenderer to use Font.Weight directly instead of UIFont.Weight conversion - Fix RichView optional unwrapping for attributedString - Remove unused uiFontWeight extension - Clean up NewsContentView and ReplyItemView (remove iOS 14/15 checks) - Re-enable full RichView rendering pipeline with caching This allows us to use modern Swift features like Regex literals and AttributedString rendering without iOS 15 compatibility constraints.
1 parent dfc2904 commit c5d3ae9

File tree

8 files changed

+84
-108
lines changed

8 files changed

+84
-108
lines changed

V2er.xcodeproj/project.pbxproj

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
1AEBC3AC5DAA63523F5448F5 /* RichContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205F350A3537A3E41B1AFC3 /* RichContentView.swift */; };
1011
28B24CA92EA3460D00F82B2A /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CA82EA3460D00F82B2A /* BalanceView.swift */; };
1112
28B24CAB2EA3561400F82B2A /* OnlineStatsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CAA2EA3561400F82B2A /* OnlineStatsInfo.swift */; };
1213
28CC76CC2E963D6700C939B5 /* FilterMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A490A3E111D941C4B30F0BACA6B5E984 /* FilterMenuView.swift */; };
1314
36A1DC574867AA711547556C /* CodeBlockAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D7F9F8E0B5EA32CC951FD5 /* CodeBlockAttachment.swift */; };
1415
3AEADD24608B3956E80DADEA /* RenderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547AFEBDC601FEDCE3364643 /* RenderConfiguration.swift */; };
1516
407E8B8C202BF0241BD99568 /* RichView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72107B2148D16415A7512930 /* RichView+Preview.swift */; };
17+
484A41DB3858F1C84507E54B /* RenderActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6356D706913919766FD0EA5 /* RenderActor.swift */; };
1618
4E55BE8A29D45FC00044389C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4E55BE8929D45FC00044389C /* Kingfisher */; };
1719
4EC32AF029D81863003A3BD4 /* WebView in Frameworks */ = {isa = PBXBuildFile; productRef = 4EC32AEF29D81863003A3BD4 /* WebView */; };
1820
4EC32AF229D818FC003A3BD4 /* WebBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */; };
@@ -155,8 +157,10 @@
155157
5DF80E3626A2D045002ADC79 /* MultilineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF80E3526A2D045002ADC79 /* MultilineTextField.swift */; };
156158
5DF92A5D26859DDD00E6086E /* HeadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF92A5C26859DDD00E6086E /* HeadIndicatorView.swift */; };
157159
62367D67C6E8AE3EC837D0F4 /* MentionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44998D879D9842BBB61D639E /* MentionParser.swift */; };
160+
9495B5E175158F3646169AA5 /* RichContentView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C4B81E79369CDE4880B773 /* RichContentView+Preview.swift */; };
158161
97B4326BB45897F25FAEBBD1 /* RenderStylesheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8366DEEFEB9819312F65353D /* RenderStylesheet.swift */; };
159162
DAC723E23F071F71DD23FC0D /* RichViewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A9E0C514C4D7E9DD4CBEE7 /* RichViewCache.swift */; };
163+
E212778C30ED41F39D51D70B /* MarkdownRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AB393F14CD383FE0EA98A9 /* MarkdownRenderer.swift */; };
160164
E6BD52539035CEA6C56D3BDF /* HTMLToMarkdownConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C732E2652C92E5D553656A9 /* HTMLToMarkdownConverter.swift */; };
161165
EC3A2A13EC68ED3A8DFA764A /* RenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3CC744901D9A994CF6ABE7 /* RenderError.swift */; };
162166
F090B4D9D3B115551BEF05B4 /* AsyncImageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E707C08835B71223A7A3359 /* AsyncImageAttachment.swift */; };
@@ -1104,6 +1108,10 @@
11041108
407E8B8C202BF0241BD99568 /* RichView+Preview.swift in Sources */,
11051109
DAC723E23F071F71DD23FC0D /* RichViewCache.swift in Sources */,
11061110
62367D67C6E8AE3EC837D0F4 /* MentionParser.swift in Sources */,
1111+
E212778C30ED41F39D51D70B /* MarkdownRenderer.swift in Sources */,
1112+
484A41DB3858F1C84507E54B /* RenderActor.swift in Sources */,
1113+
1AEBC3AC5DAA63523F5448F5 /* RichContentView.swift in Sources */,
1114+
9495B5E175158F3646169AA5 /* RichContentView+Preview.swift in Sources */,
11071115
);
11081116
runOnlyForDeploymentPostprocessing = 0;
11091117
};
@@ -1191,7 +1199,7 @@
11911199
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
11921200
GCC_WARN_UNUSED_FUNCTION = YES;
11931201
GCC_WARN_UNUSED_VARIABLE = YES;
1194-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1202+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
11951203
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
11961204
MTL_FAST_MATH = YES;
11971205
ONLY_ACTIVE_ARCH = YES;
@@ -1247,7 +1255,7 @@
12471255
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
12481256
GCC_WARN_UNUSED_FUNCTION = YES;
12491257
GCC_WARN_UNUSED_VARIABLE = YES;
1250-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1258+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
12511259
MTL_ENABLE_DEBUG_INFO = NO;
12521260
MTL_FAST_MATH = YES;
12531261
SDKROOT = iphoneos;
@@ -1269,7 +1277,7 @@
12691277
INFOPLIST_FILE = V2er/Info.plist;
12701278
INFOPLIST_KEY_CFBundleDisplayName = V2er;
12711279
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
1272-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1280+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
12731281
LD_RUNPATH_SEARCH_PATHS = (
12741282
"$(inherited)",
12751283
"@executable_path/Frameworks",
@@ -1294,7 +1302,7 @@
12941302
INFOPLIST_FILE = V2er/Info.plist;
12951303
INFOPLIST_KEY_CFBundleDisplayName = V2er;
12961304
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
1297-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1305+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
12981306
LD_RUNPATH_SEARCH_PATHS = (
12991307
"$(inherited)",
13001308
"@executable_path/Frameworks",
@@ -1315,7 +1323,7 @@
13151323
CODE_SIGN_STYLE = Automatic;
13161324
DEVELOPMENT_TEAM = 3ZMN67J68N;
13171325
INFOPLIST_FILE = V2erTests/Info.plist;
1318-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1326+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
13191327
LD_RUNPATH_SEARCH_PATHS = (
13201328
"$(inherited)",
13211329
"@executable_path/Frameworks",
@@ -1337,7 +1345,7 @@
13371345
CODE_SIGN_STYLE = Automatic;
13381346
DEVELOPMENT_TEAM = 3ZMN67J68N;
13391347
INFOPLIST_FILE = V2erTests/Info.plist;
1340-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
1348+
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
13411349
LD_RUNPATH_SEARCH_PATHS = (
13421350
"$(inherited)",
13431351
"@executable_path/Frameworks",

V2er/Sources/RichView/Cache/RichViewCache.swift

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ public class RichViewCache {
2525
private let attributedStringCache: NSCache<NSString, CachedAttributedString>
2626

2727
/// Cache for parsed content elements
28-
// TODO: Re-enable once ContentElement is available
29-
// private let contentElementsCache: NSCache<NSString, CachedContentElements>
28+
private let contentElementsCache: NSCache<NSString, CachedContentElements>
3029

3130
// MARK: - Statistics
3231

@@ -47,10 +46,9 @@ public class RichViewCache {
4746
attributedStringCache.countLimit = 100
4847

4948
// Content elements cache: 50 MB
50-
// TODO: Re-enable once ContentElement is available
51-
// contentElementsCache = NSCache<NSString, CachedContentElements>()
52-
// contentElementsCache.totalCostLimit = 50 * 1024 * 1024
53-
// contentElementsCache.countLimit = 100
49+
contentElementsCache = NSCache<NSString, CachedContentElements>()
50+
contentElementsCache.totalCostLimit = 50 * 1024 * 1024
51+
contentElementsCache.countLimit = 100
5452

5553
// Observe memory warnings
5654
NotificationCenter.default.addObserver(
@@ -116,9 +114,7 @@ public class RichViewCache {
116114
}
117115

118116
// MARK: - Content Elements Cache
119-
// TODO: Re-enable once ContentElement is available (currently in disabled RichContentView)
120117

121-
/* Disabled until ContentElement is available
122118
/// Get cached content elements for HTML
123119
public func getContentElements(forHTML html: String) -> [ContentElement]? {
124120
let key = NSString(string: cacheKey(for: html))
@@ -141,16 +137,14 @@ public class RichViewCache {
141137
let cost = elements.count * 1024 // Rough estimate
142138
contentElementsCache.setObject(cached, forKey: key, cost: cost)
143139
}
144-
*/
145140

146141
// MARK: - Cache Management
147142

148143
/// Clear all caches
149144
public func clearAll() {
150145
markdownCache.removeAllObjects()
151146
attributedStringCache.removeAllObjects()
152-
// TODO: Re-enable once ContentElement is available
153-
// contentElementsCache.removeAllObjects()
147+
contentElementsCache.removeAllObjects()
154148

155149
statsLock.lock()
156150
stats = CacheStatistics()
@@ -168,12 +162,9 @@ public class RichViewCache {
168162
}
169163

170164
/// Clear content elements cache only
171-
// TODO: Re-enable once ContentElement is available
172-
/*
173165
public func clearContentElementsCache() {
174166
contentElementsCache.removeAllObjects()
175167
}
176-
*/
177168

178169
/// Get cache statistics
179170
public func getStatistics() -> CacheStatistics {
@@ -259,7 +250,6 @@ private class CachedAttributedString {
259250
}
260251
}
261252

262-
/* Disabled until ContentElement is available
263253
private class CachedContentElements {
264254
let elements: [ContentElement]
265255
let timestamp: Date
@@ -269,7 +259,6 @@ private class CachedContentElements {
269259
self.timestamp = Date()
270260
}
271261
}
272-
*/
273262

274263
// MARK: - CommonCrypto Import
275264

V2er/Sources/RichView/Renderers/MarkdownRenderer.swift

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@
44
//
55
// Created by RichView on 2025/1/19.
66
//
7-
// TODO: This file requires iOS 16+ for Regex support
8-
// Currently disabled until iOS 15 compatible implementation is ready
9-
10-
#if false // Disabled until iOS 15 compatible
117

128
import Foundation
139
import SwiftUI
1410

1511
/// Renders Markdown content to AttributedString with styling
16-
@available(iOS 15.0, *)
12+
@available(iOS 16.0, *)
1713
public class MarkdownRenderer {
1814

1915
private let stylesheet: RenderStylesheet
@@ -113,7 +109,7 @@ public class MarkdownRenderer {
113109
default: fontSize = stylesheet.heading.h1Size
114110
}
115111

116-
attributed.font = .system(size: fontSize, weight: stylesheet.heading.fontWeight.uiFontWeight)
112+
attributed.font = .system(size: fontSize, weight: stylesheet.heading.fontWeight)
117113
attributed.foregroundColor = stylesheet.heading.color.uiColor
118114

119115
// Add spacing
@@ -263,7 +259,7 @@ public class MarkdownRenderer {
263259
let linkText = String(linkMatch.1)
264260
let linkURL = String(linkMatch.2)
265261
var linkAttributed = AttributedString(linkText)
266-
linkAttributed.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.link.fontWeight.uiFontWeight)
262+
linkAttributed.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.link.fontWeight)
267263
linkAttributed.foregroundColor = stylesheet.link.color.uiColor
268264
if stylesheet.link.underline {
269265
linkAttributed.underlineStyle = .single
@@ -290,7 +286,7 @@ public class MarkdownRenderer {
290286
// Add mention
291287
let username = String(mentionMatch.1)
292288
var mentionText = AttributedString("@\(username)")
293-
mentionText.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.mention.fontWeight.uiFontWeight)
289+
mentionText.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.mention.fontWeight)
294290
mentionText.foregroundColor = stylesheet.mention.textColor.uiColor
295291
mentionText.backgroundColor = stylesheet.mention.backgroundColor.uiColor
296292
result.append(mentionText)
@@ -310,33 +306,20 @@ public class MarkdownRenderer {
310306

311307
private func renderPlainText(_ text: String) -> AttributedString {
312308
var attributed = AttributedString(text)
313-
attributed.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.body.fontWeight.uiFontWeight)
309+
attributed.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.body.fontWeight)
314310
attributed.foregroundColor = stylesheet.body.color.uiColor
315311
return attributed
316312
}
317-
}
318313

319-
// MARK: - Extensions
320-
321-
extension Font.Weight {
322-
var uiFontWeight: UIFont.Weight {
323-
switch self {
324-
case .ultraLight: return .ultraLight
325-
case .thin: return .thin
326-
case .light: return .light
327-
case .regular: return .regular
328-
case .medium: return .medium
329-
case .semibold: return .semibold
330-
case .bold: return .bold
331-
case .heavy: return .heavy
332-
case .black: return .black
333-
default: return .regular
314+
// MARK: - Helper Methods
315+
316+
/// Extract ordered list item number and content from a line
317+
private func extractOrderedListItem(from line: String) -> (Int, String)? {
318+
guard let match = line.firstMatch(of: /^(\d+)\. (.+)/) else {
319+
return nil
334320
}
321+
let number = Int(match.1) ?? 1
322+
let content = String(match.2)
323+
return (number, content)
335324
}
336325
}
337-
338-
extension Color {
339-
var uiColor: UIColor {
340-
UIColor(self)
341-
}
342-
}#endif // false

V2er/Sources/RichView/Renderers/RenderActor.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99
import SwiftUI
1010

1111
/// Actor for thread-safe background rendering
12-
@available(iOS 15.0, *)
12+
@available(iOS 16.0, *)
1313
public actor RenderActor {
1414

1515
// MARK: - Properties
@@ -177,8 +177,6 @@ public actor RenderActor {
177177
}
178178

179179
// Image
180-
// TODO: Reimplement with iOS 15 compatible regex
181-
/* iOS 16+ only
182180
if let imageMatch = line.firstMatch(of: /!\[([^\]]*)\]\(([^)]+)\)/) {
183181
let altText = String(imageMatch.1)
184182
let urlString = String(imageMatch.2)
@@ -189,13 +187,13 @@ public actor RenderActor {
189187
index += 1
190188
continue
191189
}
192-
*/
193190

194191
// Regular text paragraph
195-
// TODO: Use MarkdownRenderer once iOS 15 compatible
196-
var attributed = AttributedString(line)
197-
attributed.font = .system(size: configuration.stylesheet.body.fontSize)
198-
attributed.foregroundColor = configuration.stylesheet.body.color
192+
let renderer = MarkdownRenderer(
193+
stylesheet: configuration.stylesheet,
194+
enableCodeHighlighting: configuration.enableCodeHighlighting
195+
)
196+
let attributed = try renderer.render(line)
199197
if !attributed.characters.isEmpty {
200198
elements.append(ContentElement(type: .text(attributed)))
201199
}

V2er/Sources/RichView/Views/RichContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import SwiftUI
99

1010
/// Enhanced RichView that properly renders images, code blocks, and complex content
11-
@available(iOS 15.0, *)
11+
@available(iOS 16.0, *)
1212
public struct RichContentView: View {
1313

1414
// MARK: - Properties

V2er/Sources/RichView/Views/RichView.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,25 @@ public struct RichView: View {
9999
)
100100
let markdown = try converter.convert(htmlContent)
101101

102-
// For iOS 15 compatibility, use simple AttributedString
103-
// TODO: Implement full MarkdownRenderer with iOS 15 compatibility
104-
var rendered = AttributedString(markdown)
105-
rendered.font = .system(size: configuration.stylesheet.body.fontSize)
106-
rendered.foregroundColor = configuration.stylesheet.body.color
107-
108-
// Update state
109-
self.attributedString = rendered
110-
self.isLoading = false
102+
// Render Markdown to AttributedString
103+
if #available(iOS 16.0, *) {
104+
let renderer = MarkdownRenderer(
105+
stylesheet: configuration.stylesheet,
106+
enableCodeHighlighting: configuration.enableCodeHighlighting
107+
)
108+
let rendered = try renderer.render(markdown)
109+
110+
// Update state
111+
self.attributedString = rendered
112+
self.isLoading = false
113+
} else {
114+
// Fallback for iOS 15 (should not happen with iOS 18 minimum)
115+
var rendered = AttributedString(markdown)
116+
rendered.font = .system(size: configuration.stylesheet.body.fontSize)
117+
rendered.foregroundColor = configuration.stylesheet.body.color
118+
self.attributedString = rendered
119+
self.isLoading = false
120+
}
111121

112122
// Create metadata
113123
let endTime = Date()
@@ -116,7 +126,7 @@ public struct RichView: View {
116126
renderTime: renderTime,
117127
htmlLength: htmlContent.count,
118128
markdownLength: markdown.count,
119-
attributedStringLength: rendered.characters.count,
129+
attributedStringLength: self.attributedString?.characters.count ?? 0,
120130
cacheHit: false,
121131
imageCount: 0,
122132
linkCount: 0,

V2er/View/FeedDetail/NewsContentView.swift

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,28 @@ struct NewsContentView: View {
2323
VStack(spacing: 0) {
2424
Divider()
2525

26-
if #available(iOS 15.0, *) {
27-
RichView(htmlContent: contentInfo?.html ?? "")
28-
.configuration(configurationForAppearance())
29-
.onLinkTapped { url in
30-
Task {
31-
await UIApplication.shared.openURL(url)
32-
}
26+
RichView(htmlContent: contentInfo?.html ?? "")
27+
.configuration(configurationForAppearance())
28+
.onLinkTapped { url in
29+
Task {
30+
await UIApplication.shared.openURL(url)
3331
}
34-
.onRenderCompleted { metadata in
35-
// Mark as rendered after content is ready
36-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
37-
self.rendered = true
38-
}
39-
}
40-
.onRenderFailed { error in
41-
print("Render error: \(error)")
32+
}
33+
.onRenderCompleted { metadata in
34+
// Mark as rendered after content is ready
35+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
4236
self.rendered = true
4337
}
44-
} else {
45-
// Fallback for iOS 14
46-
HtmlView(html: contentInfo?.html, imgs: contentInfo?.imgs ?? [], rendered: $rendered)
47-
}
38+
}
39+
.onRenderFailed { error in
40+
print("Render error: \(error)")
41+
self.rendered = true
42+
}
4843

4944
Divider()
5045
}
5146
}
5247

53-
@available(iOS 15.0, *)
5448
private func configurationForAppearance() -> RenderConfiguration {
5549
var config = RenderConfiguration.default
5650

0 commit comments

Comments
 (0)