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.
Related
So in my text editor, I'd like to know the position of the cursor geometrically. I'm also planning to append some text after that position.
So how do I do this?
Okay... So I figured out a way to do this.
First, I created a struct to store the cursor positions
import foundation
struct CursorPosition {
start: Int
end: Int
}
Then I initialize it to be static
class Global {
public static var cursorPosition = CursorPosition(start: 0, end: 0)
}
Then finally, I created a custom view to match the SwiftUI TextEditor and listen for selection change and update the CursorPosition
import UIKit
import SwiftUI
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = true
textField.backgroundColor = UIColor.clear
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var onDone: (() -> Void)?
init(text: Binding<String>, onDone: (() -> Void)? = nil) {
self.text = text
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if let range = textView.selectedTextRange {
Global.cursorPosition.start = textView.offset(from: textView.beginningOfDocument, to: range.start)
Global.cursorPosition.end = textView.offset(from: textView.beginningOfDocument, to: range.end)
}
}
}
}
struct EditText: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, onDone: onCommit)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
And using it:
EditText("", text: $text)
.onChange(of: text){ _ in
let cursorStart = Global.cursorPosition.start
}
I have a pop-up window, that shows terms of service & privacy policy for users, how to disable text selection but keep links tappable. screenshot
If I set:
SOLVED: By adding textViewDidChangeSelection method in UIViewDelegate and setting
textView.isSelectable = false
textView.isUserInteractionEnabled = true
textView.isSelectable = true
Links become uninteractable.
How do I keep the link tappable and text not selectable?
My whole class:
// HyperLinkTextView.swift
import SwiftUI
struct Hyperlink {
var word: String
var url: NSURL
}
struct HyperLinkTextView: UIViewRepresentable {
private var text: String
private var links: [Hyperlink]
let textView = UITextView()
init(text: String, links: [Hyperlink]) {
self.text = text
self.links = links
}
func makeUIView(context: Self.Context) -> UITextView {
let attributedString = NSMutableAttributedString(string: text)
links.forEach { hyperlink in
let linkAttributes = [NSAttributedString.Key.link: hyperlink.url]
var nsRange = NSMakeRange(0, 0)
if let range = text.range(of: hyperlink.word) {
nsRange = NSRange(range, in: text)
}
attributedString.setAttributes(linkAttributes, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSNumber(value: 1), range: nsRange)
}
textView.isEditable = false
textView.delegate = context.coordinator
textView.attributedText = attributedString
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Theme.colorClickables)]
textView.isUserInteractionEnabled = true
textView.isSelectable = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = UIFont(name: "ArialMT", size: 18)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
public class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
weak var textView: UITextView?
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction, replacementText text: String) -> Bool {
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}
}
}
Add this UITextViewDelegate textViewDidChangeSelection and comment out isEditable and isSelectable:
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}
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
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)
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