Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,202 @@ let clearAllItem = RichEditorOptionItem(image: UIImage(named: "clear"), title: "
toolbar.options = [clearAllItem]
```

Using RichEditorView in SwiftUI
--------------------

`RichEditorView` can be easily integrated into your SwiftUI projects using the `SwiftUIRichEditor` and `SwiftUIRichEditorToolbar` wrappers.

Start by creating a state object to manage the editor's content and properties.


![SwiftUI](./art/SwiftUI.jpg)
```swift
import SwiftUI
import RichEditorView

class RichEditorState: ObservableObject {
@Published var htmlContent: String = ""
@Published var isEditing: Bool = false
@Published var editorHeight: CGFloat = 0

weak var editor: RichEditorView?
}
```

Then, in your SwiftUI `View`, you can use `SwiftUIRichEditor` and `SwiftUIRichEditorToolbar` as follows:

```swift
import SwiftUI
import RichEditorView

struct ContentView: View {

// Rich text editor state management
@StateObject private var editorState = RichEditorState(
htmlContent: """
<ol>
<li>This is the first line for testing styles.</li>
<li><b>Bold this line.</b></li>
<li><i>Make this line italic.</i></li>
<li><u>Underline this one.</u></li>
<li><s>Apply a strikethrough here.</s></li>
<li>This is for subscript test: H<sub>2</sub>O.</li>
<li>And this is for superscript test: E=mc<sup>2</sup>.</li>
<li><font color="red">Let's test text color on this sentence.</font></li>
<li><span style="background-color: yellow;">Now, let's try a background color.</span></li>
<li><h1>Heading 1 will be applied here.</h1></li>
<li><h2>This should be Heading 2.</h2></li>
<li><h3>And here comes Heading 3.</h3></li>
<li><p style="text-indent: 40px;">Indent this paragraph.</p></li>
<li>This is a standard line.</li>
<li>This is the fifteenth item in our list.</li>
<li>A slightly longer sentence to check wrapping and alignment.</li>
<li><p style="text-align: left;">Left align this text.</p></li>
<li><p style="text-align: center;">Center align this text.</p></li>
<li><p style="text-align: right;">Right align this text.</p></li>
<li><a href="https://www.apple.com">Let's see how a hyperlink looks.</a></li>
<li>This line contains some special characters: @#$%^&amp;*().</li>
<li>A sentence with <b>multiple</b> <i>formats</i>.</li>
<li>The twenty-third line to test scrolling behavior.</li>
<li>Almost there, just one more line to go.</li>
<li>This is the final line for testing!</li>
</ol>
"""
)

// State variables for ColorPicker
@State private var foregroundColor: Color = .black
@State private var highlightColor: Color = .white

// State variables to control color picker display
@State private var showTextColorPicker: Bool = false
@State private var showBackgroundColorPicker: Bool = false

// Toolbar options configuration - using all available options
private let toolbarOptions: [RichEditorDefaultOption] = RichEditorDefaultOption.all

var body: some View {
VStack(spacing: 0) {
// Toolbar section wrapped in VStack
VStack(spacing: 0) {
SwiftUIRichEditorToolbar(
options: toolbarOptions,
barTintColor: .systemBackground,
editor: editorState.editor,
onTextColorChange: {
// Show text color picker
showTextColorPicker = true
},
onBackgroundColorChange: {
// Show background color picker
showBackgroundColorPicker = true
},
onImageInsert: {
// Handle image insertion
print("Image insertion")
},
onLinkInsert: {
// Handle link insertion
print("Link insertion")
}
)
.frame(height: 44)
.background(
Rectangle()
.fill(Color(.systemBackground))
.overlay(
Rectangle()
.stroke(Color(.separator), lineWidth: 1)
)
)
}
.padding(.horizontal, 16)
.padding(.top, 8)
.zIndex(1) // Ensure toolbar is always on top

// Divider between toolbar and editor
Divider()
.padding(.horizontal, 16)

// Editor section wrapped in VStack
VStack(spacing: 0) {
SwiftUIRichEditor(
htmlContent: $editorState.htmlContent,
isEditing: $editorState.isEditing,
placeholder: "Enter content...",
isScrollEnabled: true,
editingEnabled: true,
backgroundColor: .systemGray6,
onContentChange: { content in
print("Content changed: \(content)")
},
onHeightChange: { height in
editorState.editorHeight = CGFloat(height)
},
onEditorReady: { richEditor in
editorState.editor = richEditor
}
)
.background(Color(.systemGray6))
.clipped() // Prevent content from overflowing its bounds
}
.padding(.horizontal, 16)
.padding(.bottom, 16)
.zIndex(0) // Ensure editor is below the toolbar
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.onChange(of: foregroundColor) { newColor in
// Update editor text color when foregroundColor changes
editorState.editor?.setTextColor(UIColor(newColor))
}
.onChange(of: highlightColor) { newColor in
// Update editor text background color when highlightColor changes
editorState.editor?.setTextBackgroundColor(UIColor(newColor))
}
.sheet(isPresented: $showTextColorPicker) {
NavigationView {
VStack {
ColorPicker("Select Text Color", selection: $foregroundColor)
.padding()
Spacer()
}
.navigationTitle("Text Color")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
showTextColorPicker = false
}
}
}
}
}
.sheet(isPresented: $showBackgroundColorPicker) {
NavigationView {
VStack {
ColorPicker("Select Background Color", selection: $highlightColor)
.padding()
Spacer()
}
.navigationTitle("Background Color")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
showBackgroundColorPicker = false
}
}
}
}
}
}
}

#Preview {
ContentView()
}
```


Acknowledgements
----------------
Expand Down
138 changes: 138 additions & 0 deletions RichEditorView/Sources/RichEditorState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// RichEditorState.swift
// RichEditorView
//
// Created by yunning you on 2025/8/25.
//

import SwiftUI


/// A state management class for the rich text editor, designed for use in SwiftUI environments.
@available(iOS 13.0, *)
public class RichEditorState: ObservableObject {

// MARK: - Published Properties

/// The HTML content of the editor.
@Published public var htmlContent: String

/// A boolean value indicating whether the editor is currently focused.
@Published public var isEditing: Bool = false

/// The dynamic height of the editor content.
@Published public var editorHeight: CGFloat = 200

// MARK: - Properties

/// A weak reference to the underlying RichEditorView instance.
public var editor: RichEditorView?

// MARK: - Initialization

/// Initializes the state with optional initial HTML content.
/// - Parameter htmlContent: The initial HTML content to load into the editor.
public init(htmlContent: String = "") {
self.htmlContent = htmlContent
}

// MARK: - Public Methods

/// Clears the content of the editor.
public func clearContent() {
htmlContent = ""
editor?.html = ""
}

/// Inserts sample HTML content into the editor for demonstration purposes.
public func insertSampleContent() {
let sampleHTML = """
<h2>Welcome to the Rich Editor</h2>
<p>This is a powerful rich text editor that supports:</p>
<ul>
<li><strong>Bold</strong> and <em>italic</em> text</li>
<li><u>Underline</u> and <s>strikethrough</s></li>
<li>Multiple heading levels</li>
<li>Ordered and unordered lists</li>
<li>Text alignment</li>
<li>Text and background colors</li>
</ul>
<p>Start editing now!</p>
"""
htmlContent = sampleHTML
editor?.html = sampleHTML
}

/// Asynchronously retrieves the plain text content of the editor.
/// - Parameter completion: A closure that receives the plain text string.
public func getPlainText(completion: @escaping (String) -> Void) {
guard let editor = editor else {
completion("")
return
}
editor.getText { text in
completion(text)
}
}

/// Asynchronously retrieves content statistics, such as character and word counts.
/// - Parameter completion: A closure that receives the character count and word count.
public func getContentStats(completion: @escaping (Int, Int) -> Void) {
getPlainText { plainText in
let characters = plainText.count
let words = plainText.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }.count
completion(characters, words)
}
}

/// Saves the current HTML content to UserDefaults.
public func autoSave() {
UserDefaults.standard.set(htmlContent, forKey: "RichEditorAutoSave")
}

/// Restores the HTML content from UserDefaults if available.
public func restoreFromAutoSave() {
if let savedContent = UserDefaults.standard.string(forKey: "RichEditorAutoSave"),
!savedContent.isEmpty {
htmlContent = savedContent
editor?.html = savedContent
}
}

/// Saves the current HTML content to a file in the documents directory.
/// - Parameter filename: The name of the file to save the content to.
public func saveToFile(filename: String = "rich_editor_content.html") {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Failed to access documents directory.")
return
}
let fileURL = documentsPath.appendingPathComponent(filename)

do {
try htmlContent.write(to: fileURL, atomically: true, encoding: .utf8)
print("Content successfully saved to: \(fileURL.path)")
} catch {
print("Failed to save content: \(error)")
}
}

/// Loads HTML content from a file in the documents directory.
/// - Parameter filename: The name of the file to load the content from.
public func loadFromFile(filename: String = "rich_editor_content.html") {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Failed to access documents directory.")
return
}
let fileURL = documentsPath.appendingPathComponent(filename)

do {
let content = try String(contentsOf: fileURL, encoding: .utf8)
htmlContent = content
editor?.html = content
print("Content successfully loaded from file.")
} catch {
print("Failed to load content: \(error)")
}
}
}
Loading