Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
343 views
in Technique[技术] by (71.8m points)

ios - Save a UITextView attributed string to file in SwiftUI

I need to work with attributed strings (NSMutableAttributedString) in SwiftUI to make a simple rich-text editor, and as you might already know, attributed strings are not natively supported in SwiftUI. So I had to work with the old UITextView using a UIViewRepresentable wrapper.

Now, my app is a document-based app, and whenever I try to save the files, some strange problems happens:

First Problem: When I run the app and open a file, and start typing, the initial contents of the file are erased.

Second Problem: Whenever I write text and hit the back arrow to save file, it's never updated. All documents still have the same initial content.

image

The code for document processing is the default code that came when you create a new SwiftUI document-based app, but, I changed the encoding from plain text to NSMutableAttributedString. (I also created a document extension called .mxt instead of .txt)

Document processing file MyextDocument.swift:

import SwiftUI
import UniformTypeIdentifiers

extension UTType {
    static var MyextDocument = UTType(exportedAs: "com.example.Myext.mxt")
}

struct MyextDocument: FileDocument {
    var text: NSMutableAttributedString

    init(text: NSMutableAttributedString = NSMutableAttributedString()) {
        self.text = text
    }

    static var readableContentTypes: [UTType] { [.MyextDocument] }
    
    init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents,
              let string = try? NSMutableAttributedString(data: data, options: [NSMutableAttributedString.DocumentReadingOptionKey.documentType : NSMutableAttributedString.DocumentType.rtf], documentAttributes: nil)
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        text = string
    }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = (try? text.data(from: NSMakeRange(0, text.length), documentAttributes: [.documentType: NSMutableAttributedString.DocumentType.rtf]))!
        return .init(regularFileWithContents: data)
    }
}

UIViewRepresentable wrapper file iOSEditorTextView.swift:

import Combine
import SwiftUI
import UIKit

struct iOSEditorTextView: UIViewRepresentable {
    //@Binding var text: String
    @Binding var document: NSMutableAttributedString
    var isEditable: Bool = true
    var font: UIFont?    = .systemFont(ofSize: 14, weight: .regular)
    
    var onEditingChanged: () -> Void       = {}
    var onCommit        : () -> Void       = {}
    var onTextChange    : (String) -> Void = { _ in }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> CustomTextView {
        let textView = CustomTextView(
            text: document,
            isEditable: isEditable,
            font: font
        )
        textView.delegate = context.coordinator
        
        return textView
    }
    
    func updateUIView(_ uiView: CustomTextView, context: Context) {
        uiView.text = document
        uiView.selectedRanges = context.coordinator.selectedRanges
    }
}

// MARK: - Preview

#if DEBUG

struct iOSEditorTextView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            iOSEditorTextView(
                document: .constant(NSMutableAttributedString()),
                isEditable: true,
                font: .systemFont(ofSize: 14, weight: .regular)
            )
            .environment(.colorScheme, .dark)
            .previewDisplayName("Dark Mode")
            
            iOSEditorTextView(
                document: .constant(NSMutableAttributedString()),
                isEditable: false
            )
            .environment(.colorScheme, .light)
            .previewDisplayName("Light Mode")
        }
    }
}

#endif

// MARK: - Coordinator

extension iOSEditorTextView {
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: iOSEditorTextView
        var selectedRanges: [NSValue] = []
        
        init(_ parent: iOSEditorTextView) {
            self.parent = parent
        }
        
        func textViewDidBeginEditing(_ textView: UITextView) {
            self.parent.document = textView.attributedText as! NSMutableAttributedString
            self.parent.onEditingChanged()
        }
        
        func textViewDidChange(_ textView: UITextView) {
            self.parent.document = textView.attributedText as! NSMutableAttributedString
            //self.selectedRanges = textView.selectedRange
        }

        func textViewDidEndEditing(_ textView: UITextView) {
            self.parent.document = textView.attributedText as! NSMutableAttributedString
            self.parent.onCommit()
        }
    }
}

// MARK: - CustomTextView

final class CustomTextView: UIView, UIGestureRecognizerDelegate, UITextViewDelegate {
    private var isEditable: Bool
    private var font: UIFont?
    
    weak var delegate: UITextViewDelegate?
    
    var text: NSMutableAttributedString {
        didSet {
            textView.attributedText = text
        }
    }
    
    var selectedRanges: [NSValue] = [] {
        didSet {
            guard selectedRanges.count > 0 else {
                return
            }
            
            //textView.selectedRanges = selectedRanges
        }
    }
        
    private lazy var textView: UITextView = {
        let textView                     = UITextView(frame: .zero)
        textView.delegate                = self.delegate
        textView.font                    = self.font
        textView.isEditable              = self.isEditable
        textView.textColor               = UIColor.label
        textView.textContainerInset      = UIEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)
        textView.translatesAutoresizingMaskIntoConstraints = false
        
        return textView
    }()

    // Create paragraph styles
    let paragraphStyle = NSMutableParagraphStyle() // create paragraph style

    var attributes: [NSMutableAttributedString.Key: Any] = [
        .foregroundColor: UIColor.red,
        .font: UIFont(name: "Courier", size: 12)!
    ]
    
    // MARK: - Init
    init(text: NSMutableAttributedString, isEditable: Bool, font: UIFont?) {
        self.font       = font
        self.isEditable = isEditable
        self.text       = text
        
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Life cycle
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        setupTextView()
        
        // Set tap gesture
        let tap = UITapGestureRecognizer(target: self, action: #selector(didTapTextView(_:)))
        tap.delegate = self
        textView.addGestureRecognizer(tap)

        // create paragraph style
        self.paragraphStyle.headIndent = 108
        
        // create attributed string
        let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        
        // create attributes
        self.attributes = [
            .foregroundColor: UIColor.red,
            .font: UIFont(name: "Courier", size: 12)!,
            .paragraphStyle: paragraphStyle,
        ]

        // Create the Attributed String
        let myAttrString = NSMutableAttributedString(string: string, attributes: attributes)
        
        // Write it to the Text View
        textView.attributedText = myAttrString
    }
    
    // Show cursor and set it to position on tapping + Detect line
    @objc func didTapTextView(_ recognizer: UITapGestureRecognizer) {
        // Show cursor and set it to position on tapping
        if recognizer.state == .ended {
            textView.isEditable = true
            textView.becomeFirstResponder()
                        
            let location = recognizer.location(in: textView)
            if let position = textView.closestPosition(to: location) {
                let uiTextRange = textView.textRange(from: position, to: position)
                
                if let start = uiTextRange?.start, let end = uiTextRange?.end {
                    let loc = textView.offset(from: textView.beginningOfDocument, to: position)
                    let length = textView.offset(from: start, to: end)
                    
                    textView.selectedRange = NSMakeRange(loc, length)
                }
            }
            
        }
        
    }
        
    func setupTextView() {
        // Setup Text View delegate
        textView.delegate = self
        
        // Place the Text View on the view
        addSubview(textView)
        
        NSLayoutConstraint.activate([
            textView.topAnchor.constraint(equalTo: topAnchor),
            textView.trailingAnchor.constraint(equalTo: trailingAnchor),
            textView.leadingAnchor.constraint(equalTo: leadingAnchor),
            textView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])

    }
}

And to call the UIViewRepresentable wrapper, I wrote the following code in the ContentView:

iOSEditorTextView(
    document: $document.text,
    isEditable: true,
    font: .systemFont(ofSize: 14, weight: .regular)
)

Any help will be appreciated.

question from:https://stackoverflow.com/questions/65839021/save-a-uitextview-attributed-string-to-file-in-swiftui

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

There are several mistakes in the code.

  1. You have set the wrong delegate. So your delegate method not worked. Give self delegate, not-self. like
func setupTextView() {
    // Setup Text View delegate
    textView.delegate = delegate
  1. Use NSMutableAttributedString in the delegate.
func textViewDidBeginEditing(_ textView: UITextView) {
    self.parent.document = NSMutableAttributedString(attributedString: textView.attributedText)
    self.parent.onEditingChanged()
}

func textViewDidChange(_ textView: UITextView) {
    self.parent.document = NSMutableAttributedString(attributedString: textView.attributedText)
}

func textViewDidEndEditing(_ textView: UITextView) {
    self.parent.document = NSMutableAttributedString(attributedString: textView.attributedText)
    self.parent.onCommit()
}
  1. Remove static text from override func draw(_ rect: CGRect) this line overrides the existing text.
override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        setupTextView()
        
        // Set tap gesture
        let tap = UITapGestureRecognizer(target: self, action: #selector(didTapTextView(_:)))
        tap.delegate = self
        textView.addGestureRecognizer(tap)

        // create paragraph style
        self.paragraphStyle.headIndent = 108
        
        // create attributes
        self.attributes = [
            .foregroundColor: UIColor.red,
            .font: UIFont(name: "Courier", size: 12)!,
            .paragraphStyle: paragraphStyle,
        ]
    }

Note: Remove other code from draw rect and use init or func awakeFromNib()


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...