AttributeGraph precondition failure: setting value during update when using NSAttributedString - swift

I have this view:
struct PageView: View {
var articulos : [DetallesArticulo]
var altura : CGFloat
var body: some View {
if(articulos.count > 0)
{
TabView {
ForEach(0...articulos.count-1, id: \.self) {i in
let vistaTotal = ArticulosViewMinimum()
VStack{
//Code
}.id(UUID())
}
.padding(.all, 10)
}
.frame(width: UIScreen.main.bounds.width, height: altura)
.tabViewStyle(PageTabViewStyle())
}
}
}
ArticulosViewMinimum is a view like this:
class ArticulosViewMinimum: UIView{
required init(){
super.init(frame: CGRect.zero)
self.backgroundColor = .white
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews(){
self.addSubview(sampleText)
}
lazy var sampleText : UITextView = {
let text = UITextView()
text.attributedText = "sample text".htmlToAttributedString(size: 16, titulo: "")
return text
}()
}
It crashes with the following error message:
[error] precondition failure: setting value during update: 51768 dyld4
config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
AttributeGraph precondition failure: setting value during update:
51768.
And it crashes in my function (extension of string) to convert HTML string to NSAttributedString:
func htmlToAttributedString(size: CGFloat) -> NSAttributedString? {
var attributedString = NSAttributedString()
if(!self.isEmpty)
{
var htmlTemplate = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: -apple-system;
font-size: \(size)px;
}
</style>
</head>
<body>
\(self)
</body>
</html>
"""
guard let data = htmlTemplate.data(using: .utf8) else { return NSAttributedString()}
do {
attributedString = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
],
documentAttributes: nil)
} catch {}
}
return attributedString
}
Any idea of what's producing the crash?

Finally, I solved by having another thread to execute the init code. I don't really understand why it works, but it works.
I read that this problem is a swift bug, I hope that in the next update they solve it.
class ArticulosViewMinimum: UIView{
required init(){
super.init(frame: CGRect.zero)
self.backgroundColor = .white
DispatchQueue.main.async //<- Now works
{
setupViews()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews(){
self.addSubview(sampleText)
}
lazy var sampleText : UITextView = {
let text = UITextView()
text.attributedText = "sample text".htmlToAttributedString(size: 16, titulo: "")
return text
}()
}

Related

UIView to UIImage code is not working. It causing an runtime error

I have to convert UIView to UIImage because I want to make a custom marker.
Currently, I'm making an app using Naver Map SDK.
Here is My Code.
let marker = NMFMarker()
marker.position = NMGLatLng(lat: lat, lng: lng)
let windowView = CustomInfoWindowView()
windowView.nameLabel.text = name
windowView.jobLabel.text = job
windowView.scoreLabel.text = "\(score)"
marker.iconImage = NMFOverlayImage(image: windowView.asImage())
marker.mapView = mapView
NMFOverlayImage only supported UIImage type.
Here is My full code.
class MapViewController: UIViewController {
let viewModel = MapViewModel()
let locations = BehaviorRelay<Void>(value: ())
let disposedBag = DisposeBag()
private let imageCell = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
let mapView = NMFMapView(frame: view.frame)
view.addSubview(mapView)
bind(mapView: mapView)
}
private func bind(mapView: NMFMapView) {
let input = MapViewModel.Input(getLocations: locations.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.getLocations.asObservable().subscribe(onNext: { [unowned self] res in
if !res.isEmpty {
for i in 0..<res.count {
addMarker(
lat: res[i].address[1],
lng: res[i].address[0],
mapView: mapView,
name: res[i].name,
job: res[i].field,
score: res[i].level
)
}
}
}).disposed(by: disposedBag)
}
private func addMarker(lat: Double, lng: Double, mapView: NMFMapView, name: String, job: String, score: Double) {
let marker = NMFMarker()
marker.position = NMGLatLng(lat: lat, lng: lng)
let windowView = CustomInfoWindowView()
windowView.nameLabel.text = name
windowView.jobLabel.text = job
windowView.scoreLabel.text = "\(score)"
marker.iconImage = NMFOverlayImage(image: windowView.asImage())
marker.mapView = mapView
}
}
and this is my custom view code.
import UIKit
import SnapKit
import Then
class CustomInfoWindowView: UIView {
let customView = UIView().then {
$0.backgroundColor = .clear
}
let windowView = UIView().then {
$0.backgroundColor = R.color.mainColor()
$0.layer.cornerRadius = 10
}
let marker = UIImageView().then {
$0.image = R.image.marker()
}
let nameLabel = UILabel().then {
$0.font = .boldSystemFont(ofSize: 16)
$0.textColor = .white
}
let jobLabel = UILabel().then {
$0.font = .systemFont(ofSize: 12, weight: .medium)
$0.textColor = R.color.underLine()!
}
let starLogo = UIImageView().then {
$0.image = UIImage(systemName: "star.fill")
$0.tintColor = .systemYellow
}
let scoreLabel = UILabel().then {
$0.font = .boldSystemFont(ofSize: 14)
$0.textColor = .white
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func setUpSubViews() {
self.addSubview(customView)
[windowView, marker].forEach({customView.addSubview($0)})
[nameLabel, jobLabel, starLogo, scoreLabel].forEach({self.addSubview($0)})
customView.snp.makeConstraints {
$0.width.equalTo(108)
$0.height.equalTo(100)
}
windowView.snp.makeConstraints {
$0.width.equalToSuperview()
$0.height.equalTo(64)
$0.top.equalTo(0)
}
marker.snp.makeConstraints {
$0.width.equalTo(20)
$0.height.equalTo(27)
$0.top.equalTo(windowView.snp.bottom).offset(10)
$0.centerX.equalTo(windowView.snp.centerX)
}
nameLabel.snp.makeConstraints {
$0.top.leading.equalTo(10)
$0.width.equalTo(10)
}
jobLabel.snp.makeConstraints {
$0.centerY.equalTo(nameLabel.snp.centerY)
$0.leading.equalTo(nameLabel.snp.trailing).offset(5)
}
starLogo.snp.makeConstraints {
$0.top.equalTo(nameLabel.snp.bottom).offset(5)
$0.leading.equalTo(nameLabel.snp.leading)
$0.width.height.equalTo(15)
}
scoreLabel.snp.makeConstraints {
$0.centerY.equalTo(starLogo.snp.centerY)
$0.leading.equalTo(starLogo.snp.trailing).offset(3)
}
}
}
and this is my UIView Extension code.
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { context in
layer.render(in: context.cgContext)
}
}
}
I Use Snapkit, Then, RxSwift, RxCocoa,NMapsMap.
please Help me.
When I run the code .asImage(), It had runtime error.
enter image description here

How to share a subview in a ShareSheet in SwiftUI

I am trying to share an image of a view through the Share Sheet. This is what i am doing.
One defines an extension to UIView and View where one utilizes the UIGraphicsImageRenderer to output an UIImage and modifies the .asImage to output the picture as an UIImage.
It seems to me the simplest approach to making it work. But the image is not showing in the ShareSheet.
import SwiftUI
struct SnapshottingAPI: View {
#State private var isShareSheetShowing = false
let imageSize: CGSize = CGSize(width: 1000, height: 1000)
var body: some View {
VStack {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
.frame(width: 300, height: 300)
Text("text")
Button(action: shareButton){
Image(systemName: "square.and.arrow.up")
.frame(width: 35, height: 35)
}
}
}
func shareButton() {
isShareSheetShowing.toggle()
let highresImage = self.asImage(size: imageSize)
let outputimage = Image(uiImage: highresImage)
let items: [Any] = [outputimage]
let av = UIActivityViewController(activityItems: items, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController!.present(av, animated: true, completion: nil)
}
}
extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
}
extension View {
func asImage(size: CGSize) -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.bounds = CGRect(origin: .zero, size: size)
let image = controller.view.asImage()
return image
}
}
struct SnapshottingAPI_Previews: PreviewProvider {
static var previews: some View {
SnapshottingAPI()
}
}
I found a way to do it using some other posts. This is simple enough for me. Albeit its missing the thumbnail image in the ShareSheet
import SwiftUI
import LinkPresentation
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
struct SO1: View {
#State private var rect1: CGRect = .zero
#State private var uiimage: UIImage? = nil
#State private var isShareSheetShowing = false
var body: some View {
VStack {
HStack {
VStack {
Text("LEFT")
Text("VIEW")
}
.padding(20)
.background(Color.green)
.border(Color.blue, width: 5)
.background(RectGetter(rect: $rect1))
Button(action: shareButton){
Image(systemName: "square.and.arrow.up")
.frame(width: 35, height: 35)
}
}
}
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return nil
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let image = UIImage(named: "YourImage")!
let imageProvider = NSItemProvider(object: image)
let metadata = LPLinkMetadata()
metadata.imageProvider = imageProvider
return metadata
}
func shareButton() {
isShareSheetShowing.toggle()
self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect1)
//let outputimage = Image(uiImage: self.uiimage!)
let items: [Any] = [self.uiimage!, self, "Test Text"]
let av = UIActivityViewController(activityItems: items, applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController!.present(av, animated: true, completion: nil)
}
}
struct RectGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { proxy in
self.createView(proxy: proxy)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
struct SO1_Previews: PreviewProvider {
static var previews: some View {
SO1()
}
}

Adding padding to a UILabel with a background colour

I have a multiline UILabel that contains an NSAttributedString, and this has a background colour applied to give the above effect.
This much is fine but I need padding within the label to give a bit of space on the left. There are other posts on SO addressing this issue, such as by subclassing UILabel to add UIEdgeInsets. However, this merely added padding to the outside of the label for me.
Any suggestions on how padding can be added to this label?
EDIT: Apologies if this has been confusing, but the end goal is something like this...
Based on the answer provided here: https://stackoverflow.com/a/59216224/6257435
Just to demonstrate (several hard-coded values which would, ideally, be dynamic / calculated):
class ViewController: UIViewController, NSLayoutManagerDelegate {
var myTextView: UITextView!
let textStorage = MyTextStorage()
let layoutManager = MyLayoutManager()
override func viewDidLoad() {
super.viewDidLoad()
myTextView = UITextView()
myTextView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myTextView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
myTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
myTextView.widthAnchor.constraint(equalToConstant: 248.0),
myTextView.heightAnchor.constraint(equalToConstant: 300.0),
])
self.layoutManager.delegate = self
self.textStorage.addLayoutManager(self.layoutManager)
self.layoutManager.addTextContainer(myTextView.textContainer)
let quote = "This is just some sample text to demonstrate the word wrapping with padding at the beginning (leading) and ending (trailing) of the lines of text."
self.textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: quote)
guard let font = UIFont(name: "TimesNewRomanPSMT", size: 24.0) else {
fatalError("Could not instantiate font!")
}
let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: font]
self.textStorage.setAttributes(attributes, range: NSRange(location: 0, length: quote.count))
myTextView.isUserInteractionEnabled = false
// so we can see the frame of the textView
myTextView.backgroundColor = .systemTeal
}
func layoutManager(_ layoutManager: NSLayoutManager,
lineSpacingAfterGlyphAt glyphIndex: Int,
withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
func layoutManager(_ layoutManager: NSLayoutManager,
paragraphSpacingAfterGlyphAt glyphIndex: Int,
withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 14.0 }
}
class MyTextStorage: NSTextStorage {
var backingStorage: NSMutableAttributedString
override init() {
backingStorage = NSMutableAttributedString()
super.init()
}
required init?(coder: NSCoder) {
backingStorage = NSMutableAttributedString()
super.init(coder: coder)
}
// Overriden GETTERS
override var string: String {
get { return self.backingStorage.string }
}
override func attributes(at location: Int,
effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
return backingStorage.attributes(at: location, effectiveRange: range)
}
// Overriden SETTERS
override func replaceCharacters(in range: NSRange, with str: String) {
backingStorage.replaceCharacters(in: range, with: str)
self.edited(.editedCharacters,
range: range,
changeInLength: str.count - range.length)
}
override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
backingStorage.setAttributes(attrs, range: range)
self.edited(.editedAttributes,
range: range,
changeInLength: 0)
}
}
import CoreGraphics //Important to draw the rectangles
class MyLayoutManager: NSLayoutManager {
override init() { super.init() }
required init?(coder: NSCoder) { super.init(coder: coder) }
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, textContainer, glyphRange, stop) in
var lineRect = usedRect
lineRect.size.height = 41.0
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.saveGState()
// outline rectangles
//currentContext?.setStrokeColor(UIColor.red.cgColor)
//currentContext?.setLineWidth(1.0)
//currentContext?.stroke(lineRect)
// filled rectangles
currentContext?.setFillColor(UIColor.orange.cgColor)
currentContext?.fill(lineRect)
currentContext?.restoreGState()
}
}
}
Output (teal-background shows the frame):
I used one different way. First I get all lines from the UILabel and add extra blank space at the starting position of every line. To getting all line from the UILabel I just modify code from this link (https://stackoverflow.com/a/55156954/14733292)
Final extension UILabel code:
extension UILabel {
var addWhiteSpace: String {
guard let text = text, let font = font else { return "" }
let ctFont = CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil)
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value: ctFont, range: NSRange(location: 0, length: attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude), transform: .identity)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
guard let lines = CTFrameGetLines(frame) as? [Any] else { return "" }
return lines.map { line in
let lineRef = line as! CTLine
let lineRange: CFRange = CTLineGetStringRange(lineRef)
let range = NSRange(location: lineRange.location, length: lineRange.length)
return " " + (text as NSString).substring(with: range)
}.joined(separator: "\n")
}
}
Use:
let labelText = yourLabel.addWhiteSpace
let attributedString = NSMutableAttributedString(string: labelText)
attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.red, range: NSRange(location: 0, length: labelText.count))
yourLabel.attributedText = attributedString
yourLabel.backgroundColor = .yellow
Edited:
The above code is worked at some point but it's not sufficient. So I created one class and added padding and one shape rect layer.
class AttributedPaddingLabel: UILabel {
private let leftShapeLayer = CAShapeLayer()
var leftPadding: CGFloat = 5
var attributedTextColor: UIColor = .red
override func awakeFromNib() {
super.awakeFromNib()
self.addLeftSpaceLayer()
self.addAttributed()
}
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: 0, left: leftPadding, bottom: 0, right: 0)
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + leftPadding, height: size.height)
}
override var bounds: CGRect {
didSet {
preferredMaxLayoutWidth = bounds.width - leftPadding
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
leftShapeLayer.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: leftPadding, height: rect.height)).cgPath
}
private func addLeftSpaceLayer() {
leftShapeLayer.fillColor = attributedTextColor.cgColor
self.layer.addSublayer(leftShapeLayer)
}
private func addAttributed() {
let lblText = self.text ?? ""
let attributedString = NSMutableAttributedString(string: lblText)
attributedString.addAttribute(NSAttributedString.Key.backgroundColor, value: attributedTextColor, range: NSRange(location: 0, length: lblText.count))
self.attributedText = attributedString
}
}
How to use:
class ViewController: UIViewController {
#IBOutlet weak var lblText: AttributedPaddingLabel!
override func viewDidLoad() {
super.viewDidLoad()
lblText.attributedTextColor = .green
lblText.leftPadding = 10
}
}
SwiftUI
This is just a suggestion in SwiftUI. I hope it will be useful.
Step 1: Create the content
let string =
"""
Lorem ipsum
dolor sit amet,
consectetur
adipiscing elit.
"""
Step 2: Apply attributes
let attributed: NSMutableAttributedString = .init(string: string)
attributed.addAttribute(.backgroundColor, value: UIColor.orange, range: NSRange(location: 0, length: attributed.length))
attributed.addAttribute(.font, value: UIFont(name: "Times New Roman", size: 22)!, range: NSRange(location: 0, length: attributed.length))
Step 3: Create AttributedLabel in SwiftUI using a UILabel()
struct AttributedLabel: UIViewRepresentable {
let attributedString: NSAttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.lineBreakMode = .byClipping
label.numberOfLines = 0
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = attributedString
}
}
Final step: Use it and add .padding()
struct ContentView: View {
var body: some View {
AttributedLabel(attributedString: attributed)
.padding()
}
}
This is the result:

Replacing NSAttributedString in NSTextStorage Moves NSTextView Cursor

I followed this tutorial and created a Mac version of it. It works pretty well, except there's a bug I can't figure out. The cursor jumps to the end of the string if you try editing anything in the middle of the string. Like this:
Here is a sample project, or you can just create a new macOS project and put this in the default ViewController.swift:
import Cocoa
class ViewController: NSViewController, NSTextViewDelegate {
var textView: NSTextView!
var textStorage: FancyTextStorage!
override func viewDidLoad() {
super.viewDidLoad()
createTextView()
}
func createTextView() {
// 1
let attrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)]
let attrString = NSAttributedString(string: "This is a *cool* sample.", attributes: attrs)
textStorage = FancyTextStorage()
textStorage.append(attrString)
let newTextViewRect = view.bounds
// 2
let layoutManager = NSLayoutManager()
// 3
let containerSize = CGSize(width: newTextViewRect.width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: containerSize)
container.widthTracksTextView = true
layoutManager.addTextContainer(container)
textStorage.addLayoutManager(layoutManager)
// 4
textView = NSTextView(frame: newTextViewRect, textContainer: container)
textView.delegate = self
view.addSubview(textView)
// 5
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
And then create a FancyTextStorage class that subclasses NSTextStorage with this:
class FancyTextStorage: NSTextStorage{
let backingStore = NSMutableAttributedString()
private var replacements: [String: [NSAttributedString.Key: Any]] = [:]
override var string: String {
return backingStore.string
}
override init() {
super.init()
createHighlightPatterns()
}
func createHighlightPatterns() {
let boldAttributes = [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)]
replacements = ["(\\*\\w+(\\s\\w+)*\\*)": boldAttributes]
}
func applyStylesToRange(searchRange: NSRange) {
let normalAttrs = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13, weight: .regular), NSAttributedString.Key.foregroundColor: NSColor.init(calibratedRed: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)]
addAttributes(normalAttrs, range: searchRange)
// iterate over each replacement
for (pattern, attributes) in replacements {
do {
let regex = try NSRegularExpression(pattern: pattern)
regex.enumerateMatches(in: backingStore.string, range: searchRange) {
match, flags, stop in
// apply the style
if let matchRange = match?.range(at: 1) {
print("Matched pattern: \(pattern)")
addAttributes(attributes, range: matchRange)
// reset the style to the original
let maxRange = matchRange.location + matchRange.length
if maxRange + 1 < length {
addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
}
}
}
}
catch {
print("An error occurred attempting to locate pattern: " +
"\(error.localizedDescription)")
}
}
}
func performReplacementsForRange(changedRange: NSRange) {
var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(changedRange.location, 0)))
extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRange(for: NSMakeRange(NSMaxRange(changedRange), 0)))
beginEditing()
applyStylesToRange(searchRange: extendedRange)
endEditing()
}
override func processEditing() {
performReplacementsForRange(changedRange: editedRange)
super.processEditing()
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
print("replaceCharactersInRange:\(range) withString:\(str)")
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
//print("setAttributes:\(String(describing: attrs)) range:\(range)")
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
}
}
It seems that when the string is rewritten, it doesn't preserve the cursor position, but this same code on iOS (from the above-mentioned tutorial) doesn't have this problem.
Any ideas?
I think I (hopefully) figured it out after reading this article: https://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/
Within my ViewController.swift, I added the textDidChange delegate method and reusable function for updating the styles:
func textDidChange(_ notification: Notification) {
updateStyles()
}
func updateStyles(){
guard let fancyTextStorage = textView.textStorage as? FancyTextStorage else { return }
fancyTextStorage.beginEditing()
fancyTextStorage.applyStylesToRange(searchRange: fancyTextStorage.extendedRange)
fancyTextStorage.endEditing()
}
Then within the FancyTextStorage, I have to remove performReplacementsForRange from processEditing() because it calls applyStylesToRange() and the point of the aforementioned article is that you can't apply styles within TextStorage's processEditing() function or else the world will explode (and the cursor will move to the end).
I hope this helps someone else!

WEEX - RichText Component on iOS

I am creating a Richtext(WXTextView.swift) view component in Weex by extending WXComponent. Richtext component is currently not available in weex iOS sdk.
Here's my sample code of WXRichText.swift
import Foundation
import WeexSDK
import UIKit
class TextComponet: WXComponent {
var attributedText: NSAttributedString?
override func loadView() -> UIView {
let label = UILabel()
label.numberOfLines = 0
label.attributedText = attributedText
label.sizeToFit()
label.adjustsFontSizeToFitWidth = true
return label
}
override init(ref: String, type: String, styles: [AnyHashable : Any]?, attributes: [AnyHashable : Any]? = nil, events: [Any]?, weexInstance: WXSDKInstance) {
super.init(ref: ref, type: type, styles: styles, attributes: attributes, events: events, weexInstance: weexInstance)
if let data = attributes?["data"] as? String {
let htmlString = "<html><body>\(data)<body></html>"
attributedText = htmlString.htmlToAttributedString
}
}
func data(_ text: String) {
}
}
extension String {
var htmlToAttributedString: NSAttributedString? {
guard let data = data(using: .utf8) else { return NSAttributedString() }
do {
return try NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
} catch {
return NSAttributedString()
}
}
var htmlToString: String {
return htmlToAttributedString?.string ?? ""
}
}
and JS code
<template>
<div>
<text class="message">Hi Bramma</text>
<richText
:data="data"
>
</richText>
</div>
</template>
<script>
export default {
data () {
return {
data: '<p style="color:red">This text is normal.</p><p><b>This text is bold.</b></p>'
}
}
}
</script>
As expected, it's not showing any text on the native side through Weex.
You should return a view without a specific frame, and add width and height style in JS.