Summary
On iOS, TextView sizes itself to the width of its longest line instead of the width SwiftUI proposes. Inside a flexible container (for example ScrollView + LazyVStack with .frame(maxWidth: .infinity, alignment: .leading)), content whose lines are short collapses into a narrow column. With MarkdownText from MarkdownView 3.0.0 (which renders through TextView), an ordered list like:
1. Apple
2. Banana
3. Cherry
4. Date
5. Elderberry
produces a text view of about 98 pt in a 390 pt window. The list paragraph styles then apply their head indents inside that sliver, so item text wraps character by character (Ba- / nana) and trailing items can end up drawn outside the laid-out region entirely, which also truncates what copy(_:) returns.
macOS is not affected: the AppKit path sizes through intrinsicContentSize (height only), so the view stretches to the container width. Issue #9 describes the mirror-image behavior there.
Root cause
_TextView_UIKit.sizeThatFits(_:uiView:context:) returns uiView.sizeThatFits(...) directly:
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: InlineAttachmentTextView,
context: Context
) -> CGSize? {
uiView.sizeThatFits(
proposal.replacingUnspecifiedDimensions(
by: CGSize(
width: UIView.noIntrinsicMetric,
height: UIView.noIntrinsicMetric
)
)
)
}
UITextView.sizeThatFits returns the used (snug) width, not the proposed width. SwiftUI takes that as the definitive size for the representable, so even an explicit .frame(width: 358) on the SwiftUI side still yields a 98 pt view; the frame is wide, the text view inside is not. Long prose masks the bug because its used width equals the proposal.
Suggested fix
Honor a concrete width proposal and only use the fitted width when the proposal has none:
let fitted = uiView.sizeThatFits(
proposal.replacingUnspecifiedDimensions(
by: CGSize(
width: UIView.noIntrinsicMetric,
height: UIView.noIntrinsicMetric
)
)
)
if let width = proposal.width, width > 0 {
return CGSize(width: width, height: fitted.height)
}
return fitted
With this change applied locally (RichText main, a2fb191), the same list lays out at the full 358 pt, prose is unchanged, and copy returns the complete text. Streaming updates through MarkdownView's StreamingMarkdownReader also stay correct.
Reproduction
Self-contained XCTest against an iOS 26 simulator (iPhone 17 Pro), in a package depending on MarkdownView 3.0.0 (which pulls RichText; the same collapse reproduces with TextView directly):
import MarkdownView
import SwiftUI
import UIKit
import XCTest
@MainActor
final class WidthCollapseTests: XCTestCase {
func testListRowGetsFullWidth() async throws {
let markdown = "1. Apple\n2. Banana\n3. Cherry\n4. Date\n5. Elderberry"
let host = UIHostingController(rootView: ScrollView {
LazyVStack(alignment: .leading, spacing: 16) {
Text("another row").padding()
MarkdownText(markdown)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 16)
})
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 900))
window.rootViewController = host
window.makeKeyAndVisible()
host.view.layoutIfNeeded()
var textView: UITextView?
for _ in 0..<40 {
try await Task.sleep(for: .milliseconds(50))
textView = Self.find(in: host.view)
if let textView, textView.bounds.width > 300 { break }
}
let found = try XCTUnwrap(textView)
// Fails on current main: width is 98.0
XCTAssertGreaterThan(found.bounds.width, 300, "collapsed to \(found.bounds.width) pt")
}
private static func find(in view: UIView) -> UITextView? {
if let textView = view as? UITextView { return textView }
for sub in view.subviews {
if let found = find(in: sub) { return found }
}
return nil
}
}
Observed on current main: bounds.width == 98.0, layoutManager.usedRect width about 75 pt. Expected: the proposed width (358 pt here).
Happy to open a PR with the one-line change if you want it.
Summary
On iOS,
TextViewsizes itself to the width of its longest line instead of the width SwiftUI proposes. Inside a flexible container (for exampleScrollView+LazyVStackwith.frame(maxWidth: .infinity, alignment: .leading)), content whose lines are short collapses into a narrow column. WithMarkdownTextfrom MarkdownView 3.0.0 (which renders throughTextView), an ordered list like:produces a text view of about 98 pt in a 390 pt window. The list paragraph styles then apply their head indents inside that sliver, so item text wraps character by character (
Ba-/nana) and trailing items can end up drawn outside the laid-out region entirely, which also truncates whatcopy(_:)returns.macOS is not affected: the AppKit path sizes through
intrinsicContentSize(height only), so the view stretches to the container width. Issue #9 describes the mirror-image behavior there.Root cause
_TextView_UIKit.sizeThatFits(_:uiView:context:)returnsuiView.sizeThatFits(...)directly:UITextView.sizeThatFitsreturns the used (snug) width, not the proposed width. SwiftUI takes that as the definitive size for the representable, so even an explicit.frame(width: 358)on the SwiftUI side still yields a 98 pt view; the frame is wide, the text view inside is not. Long prose masks the bug because its used width equals the proposal.Suggested fix
Honor a concrete width proposal and only use the fitted width when the proposal has none:
With this change applied locally (RichText
main, a2fb191), the same list lays out at the full 358 pt, prose is unchanged, and copy returns the complete text. Streaming updates throughMarkdownView'sStreamingMarkdownReaderalso stay correct.Reproduction
Self-contained XCTest against an iOS 26 simulator (iPhone 17 Pro), in a package depending on
MarkdownView3.0.0 (which pulls RichText; the same collapse reproduces withTextViewdirectly):Observed on current
main:bounds.width == 98.0,layoutManager.usedRectwidth about 75 pt. Expected: the proposed width (358 pt here).Happy to open a PR with the one-line change if you want it.