将 UITextView 属性字符串保存到 SwiftUI 中的文件
Save a UITextView attributed string to file in SwiftUI
我需要在 SwiftUI
中使用属性字符串 (NSMutableAttributedString
) 来制作一个简单的富文本编辑器,您可能已经知道,SwiftUI 本身不支持属性字符串。所以我不得不使用旧的 UITextView
使用 UIViewRepresentable
包装器。
现在,我的应用程序是一个基于文档的应用程序,每当我尝试保存文件时,都会发生一些奇怪的问题:
第一个问题:当我运行应用程序打开一个文件并开始输入时,文件的初始内容被删除。
第二个问题: 每当我写文本并点击后退箭头保存文件时,它永远不会更新。所有文档仍然具有相同的初始内容。
文档处理代码是创建新的基于 SwiftUI 文档的应用程序时出现的默认代码,但是,我将编码从纯文本更改为 NSMutableAttributedString
。 (我还创建了一个名为 .mxt 而不是 .txt 的文档扩展名)
文档处理文件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 包装文件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)
])
}
}
为了调用 UIViewRepresentable 包装器,我在 ContentView
中编写了以下代码:
iOSEditorTextView(
document: $document.text,
isEditable: true,
font: .systemFont(ofSize: 14, weight: .regular)
)
我们将不胜感激。
代码中有几处错误。
- 您设置了错误的委托。所以你的委托方法不起作用。
给自我委托,而不是自我。喜欢
func setupTextView() {
// Setup Text View delegate
textView.delegate = delegate
- 在委托中使用
NSMutableAttributedString
。
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()
}
- 从
override func draw(_ rect: CGRect)
中删除静态文本此行将覆盖现有文本。
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,
]
}
注意:从 draw rect 中删除其他代码并使用 init 或 func awakeFromNib()
Raja 的所有问题都是正确的。我会做的另一件事是不将 struct
值作为委托传递给您的 Coordinator
,因为您不能保证它的相同实例稍后可用。相反,将绑定传递给可变字符串要好得多。所以:
extension iOSEditorTextView {
class Coordinator: NSObject, UITextViewDelegate {
var documentBinding : Binding<NSMutableAttributedString>
var selectedRanges: [NSValue] = []
init(_ documentBinding: Binding<NSMutableAttributedString>) {
self.documentBinding = documentBinding
}
func textViewDidBeginEditing(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
}
func textViewDidChange(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
//self.selectedRanges = textView.selectedRange
}
func textViewDidEndEditing(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
}
}
}
然后:
func makeCoordinator() -> Coordinator {
Coordinator($document)
}
我需要在 SwiftUI
中使用属性字符串 (NSMutableAttributedString
) 来制作一个简单的富文本编辑器,您可能已经知道,SwiftUI 本身不支持属性字符串。所以我不得不使用旧的 UITextView
使用 UIViewRepresentable
包装器。
现在,我的应用程序是一个基于文档的应用程序,每当我尝试保存文件时,都会发生一些奇怪的问题:
第一个问题:当我运行应用程序打开一个文件并开始输入时,文件的初始内容被删除。
第二个问题: 每当我写文本并点击后退箭头保存文件时,它永远不会更新。所有文档仍然具有相同的初始内容。
文档处理代码是创建新的基于 SwiftUI 文档的应用程序时出现的默认代码,但是,我将编码从纯文本更改为 NSMutableAttributedString
。 (我还创建了一个名为 .mxt 而不是 .txt 的文档扩展名)
文档处理文件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 包装文件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)
])
}
}
为了调用 UIViewRepresentable 包装器,我在 ContentView
中编写了以下代码:
iOSEditorTextView(
document: $document.text,
isEditable: true,
font: .systemFont(ofSize: 14, weight: .regular)
)
我们将不胜感激。
代码中有几处错误。
- 您设置了错误的委托。所以你的委托方法不起作用。 给自我委托,而不是自我。喜欢
func setupTextView() {
// Setup Text View delegate
textView.delegate = delegate
- 在委托中使用
NSMutableAttributedString
。
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()
}
- 从
override func draw(_ rect: CGRect)
中删除静态文本此行将覆盖现有文本。
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,
]
}
注意:从 draw rect 中删除其他代码并使用 init 或 func awakeFromNib()
Raja 的所有问题都是正确的。我会做的另一件事是不将 struct
值作为委托传递给您的 Coordinator
,因为您不能保证它的相同实例稍后可用。相反,将绑定传递给可变字符串要好得多。所以:
extension iOSEditorTextView {
class Coordinator: NSObject, UITextViewDelegate {
var documentBinding : Binding<NSMutableAttributedString>
var selectedRanges: [NSValue] = []
init(_ documentBinding: Binding<NSMutableAttributedString>) {
self.documentBinding = documentBinding
}
func textViewDidBeginEditing(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
}
func textViewDidChange(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
//self.selectedRanges = textView.selectedRange
}
func textViewDidEndEditing(_ textView: UITextView) {
documentBinding.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
}
}
}
然后:
func makeCoordinator() -> Coordinator {
Coordinator($document)
}