SwiftUI attributed string from HTML crashes the app - ios

I'm trying to convert HTML formatted text into an attributed string, and insert it into a SwiftUI view.
Firstly I have a String extension that converts the HTML string to NSAttributedString:
extension String {
func convertHtml() -> NSAttributedString {
guard let data = data(using: .utf8) else { return NSAttributedString() }
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
return attributedString
} else {
return NSAttributedString()
}
}
}
Then I have created a HTMLLabel view:
struct HTMLLabel: UIViewRepresentable {
let html: String
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let label = UILabel()
label.attributedText = html.convertHtml()
return label
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Self>) {}
}
Then I insert a test into my SwiftUI view:
HTMLLabel(html: "<html><body><b>Hello</b> <i>World</i></body></html>")
The code crashes each time on if let attributedString = try? ... with EXC_BAD_INSTRUCTION. I made a test in an empty storyboard project like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: CGRect(x: 50, y: 50, width: 320, height: 50))
label.attributedText = "<html><body><b>Hello</b> <i>World</i></body></html>".convertHtml()
view.addSubview(label)
}
}
That code executes without any issues. Why doesn't the code work in a SwiftUI context?

Use This:-
struct HTMLLabel: UIViewRepresentable {
let html: String
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let label = UILabel()
DispatchQueue.main.async {
if let attributedText = self.html.convertHtml() {
label.attributedText = attributedText
}
}
return label
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Self>) {}
}
NSAttributedString.DocumentType.html is Only Work with Main Thread That's why you are getting crash

Related

PresentationDragIndicator disappears in dark mode

PresentationDrag Indicator disappears in dark mode. It does not disappear when used in a common View. However, when I use it in htmlView(), this phenomenon appears. (Even if I set Css for dark mode in htmlView, the result is the same.) I would be grateful if you could tell me how to solve this. Is there any way to control the style of presentationDetents (frame) or PresentationDragIndicator?
...
...
.sheet(isPresented: $activeSheet) {
VStack(){
HTMLView(html: urlToOpenInSheet)
.presentationDetents([.medium, .large])
.padding(5)
}
}
...
...
struct HTMLView: UIViewRepresentable {
let html: String
class CoordinatorHTML : NSObject, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
DispatchQueue.main.async {
let addCss = "<head><style type=\"text/css\">" +
"""
#font-face {
font-family: "Avenir";
}
body {font-family: "Avenir"; font-size: 14px; line-height: 1.0; margin: 30px;}
"""
+ " </style></head>" + "<body>" + html + "</body>"
let data = Data(addCss.utf8)
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) {
uiView.isEditable = false
uiView.isSelectable = true
uiView.attributedText = attributedString
}
}
}
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let uiTextView = UITextView()
uiTextView.delegate = context.coordinator
uiTextView.isScrollEnabled = true
uiTextView.backgroundColor = UIColor.white
return uiTextView
}
func makeCoordinator() -> CoordinatorHTML {
return CoordinatorHTML()
}
}
Light Mode Image.
Dark Mode Image.

Data Sharing Between My App and App Extensions

I transfer data from the sharing extension to my main application with UserDefaults and open the application (goToApp()) after hitting the "post" button. However, the view of my app is not redrawn and the text remains the same "Share Extension Example". Here's how I'm trying to do it:
class ShareViewController: SLComposeServiceViewController {
private var textString: String?
override func isContentValid() -> Bool {
if let currentMessage = contentText {
self.textString = currentMessage
}
return true
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didSelectPost() {
UserDefaults.standard.set(self.textString!, forKey: "text")
gotoApp()
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func gotoApp() {
guard let url = URL(string: "example://") else { return }
let selectorOpenURL = sel_registerName("openURL:")
var responder: UIResponder? = self
while responder != nil {
if responder?.responds(to: selectorOpenURL) == true {
responder?.perform(selectorOpenURL, with: url)
}
responder = responder?.next
}
}
}
And the project to which I am trying to transfer data:
class ViewController: UIViewController {
private let mainVStack = UIStackView()
private let backgroundView = UIImageView()
private let titleLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
configureMainStack()
configureTitleLabel()
}
}
// MARK: - UI Elements
private extension ViewController {
func configureMainStack() {
mainVStack.distribution = .fillProportionally
mainVStack.embed(asSubviewTo: view, inset: 40)
}
func configureTitleLabel() {
titleLabel.textAlignment = .center
titleLabel.textColor = .blue
if let text = UserDefaults.object(forKey: "text") as? String {
titleLabel.text = text
} else {
titleLabel.text = "Share Extension Example"
}
let titleContainerView = UIView()
titleLabel.embedIn(titleContainerView, hInset: 0, vInset: 100)
mainVStack.addArrangedSubview(titleContainerView)
}
}

Color in HTML string not updated when changing dark mode

I'm using SwiftUI, with a custom view for displaying HTML text in a Text :
struct HTMLText: UIViewRepresentable {
private let html: String
private let label = UILabel()
init(html: String, font: String? = nil, size: CGFloat? = nil) {
self.html = html
}
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
DispatchQueue.main.async {
let data = Data(html.utf8)
if let attributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes:nil) {
label.attributedText = attributedString
}
}
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {}
}
In my main view, I'm using it that way :
struct ConsonantListItem: View {
let consonant: Consonant
var color : String = "#\(UIColor(named: "DefaultText")?.getHexString() ?? "")"
var body: some View {
VStack {
HTMLText(html: "<div style=\"text-align:center\"><span style=\"font-size:20px;font-family:'Helvetica Neue';color:\(self.color)\">\(consonant.consonantRtgs)</span></div>")
.frame(height: 20)
// ...
}
}
}
If I'm running my application, this is working pretty well, and the color is the right one, wether I'm in light or dark mode (DefaultText is a color from assets which is black in light mode, and white in dark mode). But when I'm switching mode in the settings, then the color of this component is not updated. I tried to put the color with #State before, but nothing changed.
All the other colors are updated, but the only solution for this component is to kill and restart the app to make it the right color.
Am I missing something?
Consider managing the color not inside html but by means of Swift. Also you might have to add the same code in updateUIView to trigger the changes. See the modified code:
struct HTMLText: UIViewRepresentable {
private let html: String
init(html: String, font: String? = nil, size: CGFloat? = nil) {
self.html = html
}
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let label = UILabel()
setLabelText(label: label)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
setLabelText(label: uiView)
}
private func setLabelText(label: UILabel) {
DispatchQueue.main.async {
let data = Data(html.utf8)
if let attributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes:nil) {
label.attributedText = attributedString
label.backgroundColor = UIColor.systemBackground
label.textColor = UIColor.label
}
}
}
}
Instead of UIColor.systemBackground and UIColor.label you can use custom colors for light/dark modes like this - colorScheme == .dark ? Color.black : Color.white. For that you will also need to declare colorScheme in your struct this way - #Environment(\.colorScheme) var colorScheme

UITextView with NSMutableAttributedString with Link SwiftUI

I have this Custom UITextView that uses "ShouldInteractWith" method:
class StudyText: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
}
struct ClickableText: UIViewRepresentable {
#Binding var text: NSMutableAttributedString
func makeUIView(context: Context) -> StudyText {
let view = StudyText()
view.dataDetectorTypes = .all
view.isEditable = false
view.isSelectable = true
view.delegate = view
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: StudyText, context: Context) {
uiView.attributedText = text
}
}
I have this extension to set a link to the attributed text:
extension NSMutableAttributedString {
func apply(link: String, subString: String) {
if let range = self.string.range(of: subString) {
self.apply(link: link, onRange: NSRange(range, in: self.string))
}
}
private func apply(link: String, onRange: NSRange) {
self.addAttributes([NSAttributedString.Key.link: link], range: onRange)
}
}
And I created this layout:
struct contentView: View {
#State text: NSMutableAttributedString = NSMutableAttributedString(string: "")
var body: some View {
VStack {
ClickableText(text: self.$text)
}
.onAppear{
let myText = "Click Me!"
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "Soeme random link", subString: myText)
self.text = attributedString
}
}
}
When I click on the text view it doesn't print anything to the console and sometimes it crashes.
How can I fix this?
It must be provided valid URL, like
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "https://www.google.com", subString: myText)

How to set change placeholder color on searchbar?

extension UISearchBar {
private func getViewElement<T>(type: T.Type) -> T? {
let svs = subviews.flatMap { $0.subviews }
guard let element = (svs.filter { $0 is T }).first as? T else { return nil }
return element
}
func getSearchBarTextField() -> UITextField? {
return getViewElement(type: UITextField.self)
}
func setTextColor(color: UIColor) {
if let textField = getSearchBarTextField() {
textField.textColor = color
}
}
func setTextFieldColor(color: UIColor) {
if let textField = getViewElement(type: UITextField.self) {
switch searchBarStyle {
case .minimal:
textField.layer.backgroundColor = color.cgColor
textField.layer.cornerRadius = 6
case .prominent, .default:
textField.backgroundColor = color
}
}
}
func setPlaceholderTextColor(color: UIColor) {
if let textField = getSearchBarTextField() {
textField.attributedPlaceholder = NSAttributedString(string: self.placeholder != nil ? self.placeholder! : "", attributes: [NSAttributedString.Key.foregroundColor:color])
}
}
func setPlaceholderfont(fontfamily: UIFont) {
if let textField = getSearchBarTextField() {
textField.attributedPlaceholder = NSAttributedString(string: self.placeholder != nil ? self.placeholder! : "", attributes: [NSAttributedString.Key.font:fontfamily])
}
}
}
I have created an extension for Uisearchbbar so that i can able to customize the placeholder
In extension setPlaceholderfont() is working fine
when I call the below method the color of the placeholder text is not changing
pick.searchBar.setPlaceholderTextColor(color: Server().primarycolor)
When you call the two methods to update the attributed placeholders' color and font, you're overwriting one with the last. So assuming you're setting the color first and the font second, the color attribute is removed when setting the font attribute.
A simple way to avoid this is start with a single point of entry that updates the placeholder attributes, which is used by both the color and font setter methods. I have used a computed property that has both a getter and setter – it can get and set the current attributes of the placeholder.
extension UITextField {
var placeholderColor: UIColor? {
get { return placeholderAttributes?[.foregroundColor] as? UIColor }
set { placeholderAttributes?[.foregroundColor] = newValue }
}
var placeholderFont: UIFont? {
get { return placeholderAttributes?[.font] as? UIFont }
set { placeholderAttributes?[.font] = newValue }
}
var placeholderAttributes: [NSAttributedString.Key: Any]? {
get { return attributedPlaceholder?.attributes(at: 0, effectiveRange: nil) }
set { attributedPlaceholder = .init(string: placeholder ?? "", attributes: newValue) }
}
}
So cut back to your UISearchBar extension (I would replace the getSearchBarTextField method with a computed property textField), we can remove any reference to attributed strings etc.
var textField: UITextField? {
return getViewElement(type: UITextField.self)
}
func setTextColor(color: UIColor) {
textField?.textColor = color
}
func setPlaceholderTextColor(color: UIColor) {
textField?.placeholderColor = color
}
func setPlaceholderFont(font: UIFont) {
textField?.placeholderFont = font
}
Although properties with the appropriate types (UIColor, UIFont) may come in handy in some situations, you technically don't need the placeholderColor and placeholderFont properties, as you can just set them using the text field placeholderAttributes property from the extension.
extension UITextField {
var placeholderAttributes: [NSAttributedString.Key: Any]? {
get { return attributedPlaceholder?.attributes(at: 0, effectiveRange: nil) }
set { attributedPlaceholder = .init(string: placeholder ?? "", attributes: newValue) }
}
}
extension UISearchBar {
// ...
func setPlaceholderTextColor(color: UIColor) {
textField?.placeholderAttributes?[.foregroundColor] = color
}
func setPlaceholderfont(fontfamily: UIFont) {
textField?.placeholderAttributes?[.font] = fontfamily
}
}

Resources