iOS中使用enumerateAttributes调整TabStops,解决UITextView嵌套列表边距问题
I’ve run into this exact issue before—UIKit’s default HTML parser tends to stack up tab intervals for nested <ul> elements, leading to that wonky over-indented look you’re seeing, and CSS padding/margin fixes often don’t play nice with UITextView’s attributed string rendering. Your current code adjusts tab stops once, but it doesn’t account for how nested levels multiply the indentation. Here’s how to fix it:
Step 1: Adjust HTML Conversion with Base CSS (Optional but Helpful)
First, tweak your HTML before converting it to an attributed string to reset default list margins/padding. This cuts down on UIKit’s built-in spacing that’s hard to override later:
func convertHtmlToAttributedString(_ html: String) -> NSAttributedString? { // Inject CSS to reset list styles at the source let styledHtml = html.replacingOccurrences( of: "<ul>", with: "<ul style=\"margin: 0; padding-left: 20px;\">" ) guard let data = styledHtml.data(using: .utf8) else { return nil } let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ .documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue, .defaultAttributes: [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14, weight: .regular) ] ] do { return try NSAttributedString(data: data, options: options, documentAttributes: nil) } catch { print("HTML conversion error: \(error.localizedDescription)") return nil } }
Step 2: Fix Tab Stops & Indentation via Swift Code
The core fix is calculating the nesting level of each list item and setting consistent indentation per level, instead of just subtracting a single tab interval. Replace your existing attributed string processing code with this:
guard let attributedQuestionMessage = convertHtmlToAttributedString(yourHtmlString) else { return } let mutableAttributedString = NSMutableAttributedString(attributedString: attributedQuestionMessage) let indentPerLevel: CGFloat = 20.0 // Adjust this to control how much each nested level indents mutableAttributedString.enumerateAttributes( in: NSRange(location: 0, length: mutableAttributedString.length), options: [] ) { (attributes, range, _) in guard let originalStyle = attributes[.paragraphStyle] as? NSParagraphStyle else { return } let mutableStyle = originalStyle.mutableCopy() as! NSMutableParagraphStyle // Calculate nesting level: UIKit adds one tab stop per nested list level let nestingLevel = mutableStyle.tabStops.count // Reset tab stops to match our consistent per-level indent var adjustedTabs: [NSTextTab] = [] for level in 0..<nestingLevel { let tabLocation = indentPerLevel * CGFloat(level + 1) adjustedTabs.append(NSTextTab(textAlignment: .left, location: tabLocation, options: [:])) } // Set indents to align list content properly mutableStyle.firstLineHeadIndent = indentPerLevel * CGFloat(nestingLevel) mutableStyle.headIndent = indentPerLevel * CGFloat(nestingLevel) mutableStyle.tabStops = adjustedTabs mutableStyle.defaultTabInterval = indentPerLevel // Update the attributed string with our fixed style mutableAttributedString.removeAttribute(.paragraphStyle, range: range) mutableAttributedString.addAttribute(.paragraphStyle, value: mutableStyle, range: range) } textView.attributedText = mutableAttributedString
How This Works:
- Nesting Level Detection: UIKit automatically adds a tab stop for each nested
<ul>layer, so we use the count of tab stops to determine how deep a list item is. - Consistent Indentation: Instead of letting UIKit stack default tab intervals, we explicitly set each tab stop to
indentPerLevel * (level + 1), ensuring each nested level indents by the same amount. - Indent Alignment: Setting
firstLineHeadIndentandheadIndentensures the list content lines up correctly with the bullet points, no matter how deep the nesting goes.
This should eliminate those extra padding gaps and fix the misalignment in your nested lists.
内容的提问来源于stack exchange,提问作者Zubair

