How to Use an Nsattributedstring with a Scrollview in Swiftui

How to use an NSAttributedString with a ScrollView in SwiftUI?

The reason is that SwiftUI ScrollView requires defined content size, but used UITextView is itself a UIScrollView and detects content based on available space in parent view. Thus it happens cycle of undefined sizes.

Here is a simplified demo of possible approach how to solve this. The idea is to calculate content size of UITextView and pass it to SwiftUI...

struct TextWithAttributedString: UIViewRepresentable {
@Binding var height: CGFloat
var attributedString: NSAttributedString

func makeUIView(context: Context) -> UITextView {
let textView = UITextView(frame: .zero)
textView.isEditable = false
return textView
}

func updateUIView(_ textView: UITextView, context: Context) {
textView.attributedText = self.attributedString

// calculate height based on main screen, but this might be
// improved for more generic cases
DispatchQueue.main.async { // << fixed
height = textView.sizeThatFits(UIScreen.main.bounds.size).height
}
}
}


struct NSAttributedStringView: View {
@State private var textHeight: CGFloat = .zero
var body: some View {
ScrollView {
TextWithAttributedString(height: $textHeight, attributedString: NSAttributedString(string: exampleText))
.frame(height: textHeight) // << specify height explicitly !
}
}
}

UITextView and NSAttributedString is not showing in SwiftUI

This problem is invoked by ScrollView.

My TextView() is wrapped by ScrollView in higher view.

You can find answer in this link -> How to use an NSAttributedString with a ScrollView in SwiftUI?

How to use Attributed String in SwiftUI

iOS 15 and Swift 5.5

Text now supports markdown and also you can create custom attributes:

Sample Image

You can even get defined attributes remotely like:

Sample Image



iOS 13 and 14

You can combine multiple Text objects together with a simple + operator and that will handle some of the attributions:

Sample Image

Each one can have multiple and specific modifiers



A fully supported fallback!

Since it doesn't support directly on Text (till iOS 15), you can bring the UILabel there and modify it in anyway you like:

Implementation:

struct UIKLabel: UIViewRepresentable {

typealias TheUIView = UILabel
fileprivate var configuration = { (view: TheUIView) in }

func makeUIView(context: UIViewRepresentableContext<Self>) -> TheUIView { TheUIView() }
func updateUIView(_ uiView: TheUIView, context: UIViewRepresentableContext<Self>) {
configuration(uiView)
}
}

Usage:

var body: some View {
UIKLabel {
$0.attributedText = NSAttributedString(string: "HelloWorld")
}
}

SwiftUI: Showing HTML text inside a ScrollView

As WKWebView has already scroll and you are also wrapped inside the scroll view, so parent scroll view not get the proper size.
You have to disable WKWebView scrolling. Also, bind the size with webview and update the frame of webview.

Here is the possible demo.

struct AttributedText: UIViewRepresentable {
let htmlContent: String
@Binding var size: CGSize

private let webView = WKWebView()
var sizeObserver: NSKeyValueObservation?

func makeUIView(context: Context) -> WKWebView {
webView.scrollView.isScrollEnabled = false //<-- Here
webView.navigationDelegate = context.coordinator
return webView
}

func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}

class Coordinator: NSObject, WKNavigationDelegate {
let parent: AttributedText
var sizeObserver: NSKeyValueObservation?

init(parent: AttributedText) {
self.parent = parent
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
parent.size = change.newValue ?? .zero
})
}
}
}

For view

@State private var size: CGSize = .zero

var body: some View{
ScrollView {
AttributedText(htmlContent: "<h1>This is HTML String</h1>", size: $size)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

Binding with RichTextField (i.e. NSTextView in NSViewRepresentable) - Value resets after redraw, LostFocus, etc

Not sure if it's the correct way to do it, but I got the binding to work with the following code:

import Foundation
import SwiftUI


struct RichTextField: NSViewRepresentable {
typealias NSViewType = NSTextView

@Binding var attributedString: NSAttributedString
var isEditable: Bool

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView(frame: .zero)

textView.textStorage?.setAttributedString(self.attributedString)
textView.isEditable = isEditable
textView.delegate = context.coordinator

textView.translatesAutoresizingMaskIntoConstraints = false
textView.autoresizingMask = [.width, .height]

return textView
}

func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.textStorage!.setAttributedString(self.attributedString)
}

// Source: https://medium.com/fantageek/use-xib-de9d8a295757
class Coordinator: NSObject, NSTextViewDelegate {
let parent: RichTextField

init(_ RichTextField: RichTextField) {
self.parent = RichTextField
}

func textDidChange(_ notification: Notification) {

guard let textView = notification.object as? NSTextView else { return }
self.parent.attributedString = textView.attributedString()

}

}

}

(I still get the "cycle detected" error when using the RichTextField in a List (non-editable) though..)



Related Topics



Leave a reply



Submit