diff --git a/README.md b/README.md index a31c3321..913af8e4 100755 --- a/README.md +++ b/README.md @@ -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: """ +
    +
  1. This is the first line for testing styles.
  2. +
  3. Bold this line.
  4. +
  5. Make this line italic.
  6. +
  7. Underline this one.
  8. +
  9. Apply a strikethrough here.
  10. +
  11. This is for subscript test: H2O.
  12. +
  13. And this is for superscript test: E=mc2.
  14. +
  15. Let's test text color on this sentence.
  16. +
  17. Now, let's try a background color.
  18. +
  19. Heading 1 will be applied here.

  20. +
  21. This should be Heading 2.

  22. +
  23. And here comes Heading 3.

  24. +
  25. Indent this paragraph.

  26. +
  27. This is a standard line.
  28. +
  29. This is the fifteenth item in our list.
  30. +
  31. A slightly longer sentence to check wrapping and alignment.
  32. +
  33. Left align this text.

  34. +
  35. Center align this text.

  36. +
  37. Right align this text.

  38. +
  39. Let's see how a hyperlink looks.
  40. +
  41. This line contains some special characters: @#$%^&*().
  42. +
  43. A sentence with multiple formats.
  44. +
  45. The twenty-third line to test scrolling behavior.
  46. +
  47. Almost there, just one more line to go.
  48. +
  49. This is the final line for testing!
  50. +
+ """ + ) + + // 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 ---------------- diff --git a/RichEditorView/Sources/RichEditorState.swift b/RichEditorView/Sources/RichEditorState.swift new file mode 100644 index 00000000..bb3a9118 --- /dev/null +++ b/RichEditorView/Sources/RichEditorState.swift @@ -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 = """ +

Welcome to the Rich Editor

+

This is a powerful rich text editor that supports:

+ +

Start editing now!

+ """ + 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)") + } + } +} diff --git a/RichEditorView/Sources/SwiftUIRichEditor.swift b/RichEditorView/Sources/SwiftUIRichEditor.swift new file mode 100644 index 00000000..fa1468be --- /dev/null +++ b/RichEditorView/Sources/SwiftUIRichEditor.swift @@ -0,0 +1,272 @@ +// +// SwiftUIRichEditor.swift +// RichEditorView +// +// Created by SwiftUI Integration +// + +import SwiftUI +import UIKit + +// MARK: - SwiftUI包装器 + +/// SwiftUI版本的富文本编辑器 +/// 支持独立使用或与工具栏配合使用 +@available(iOS 13.0, *) +public struct SwiftUIRichEditor: UIViewRepresentable { + + // MARK: - 绑定属性 + @Binding private var htmlContent: String + @Binding private var isEditing: Bool + + // MARK: - 配置属性 + private let placeholder: String + private let isScrollEnabled: Bool + private let editingEnabled: Bool + private let backgroundColor: UIColor + private let onContentChange: ((String) -> Void)? + private let onHeightChange: ((Int) -> Void)? + private let onFocusChange: ((Bool) -> Void)? + private let onEditorReady: ((RichEditorView) -> Void)? + + // MARK: - 初始化器 + + /// 基本初始化器 + /// - Parameters: + /// - htmlContent: HTML内容的绑定 + /// - isEditing: 编辑状态的绑定 + /// - placeholder: 占位符文本 + /// - isScrollEnabled: 是否允许滚动 + /// - editingEnabled: 是否允许编辑 + /// - backgroundColor: 背景颜色 + /// - onContentChange: 内容变化回调 + /// - onHeightChange: 高度变化回调 + /// - onFocusChange: 焦点变化回调 + /// - onEditorReady: 编辑器准备就绪回调 + public init( + htmlContent: Binding, + isEditing: Binding = .constant(false), + placeholder: String = "请输入内容...", + isScrollEnabled: Bool = true, + editingEnabled: Bool = true, + backgroundColor: UIColor = .systemBackground, + onContentChange: ((String) -> Void)? = nil, + onHeightChange: ((Int) -> Void)? = nil, + onFocusChange: ((Bool) -> Void)? = nil, + onEditorReady: ((RichEditorView) -> Void)? = nil + ) { + self._htmlContent = htmlContent + self._isEditing = isEditing + self.placeholder = placeholder + self.isScrollEnabled = isScrollEnabled + self.editingEnabled = editingEnabled + self.backgroundColor = backgroundColor + self.onContentChange = onContentChange + self.onHeightChange = onHeightChange + self.onFocusChange = onFocusChange + self.onEditorReady = onEditorReady + } + + // MARK: - UIViewRepresentable实现 + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeUIView(context: Context) -> RichEditorView { + let editor = RichEditorView() + + // 基本配置 + editor.delegate = context.coordinator + editor.placeholder = placeholder + editor.isScrollEnabled = isScrollEnabled + editor.editingEnabled = editingEnabled + editor.setEditorBackgroundColor(backgroundColor) + + // 设置初始内容 + if !htmlContent.isEmpty { + editor.html = htmlContent + } + + // 通知编辑器已准备就绪 + onEditorReady?(editor) + + return editor + } + + public func updateUIView(_ uiView: RichEditorView, context: Context) { + // 更新内容(避免循环更新) + if uiView.contentHTML != htmlContent { + uiView.html = htmlContent + } + + // 更新编辑状态 + if uiView.editingEnabled != editingEnabled { + uiView.editingEnabled = editingEnabled + } + + // 更新占位符 + if uiView.placeholder != placeholder { + uiView.placeholder = placeholder + } + } +} + +// MARK: - Coordinator +@available(iOS 13.0, *) +extension SwiftUIRichEditor { + public class Coordinator: NSObject, RichEditorDelegate { + var parent: SwiftUIRichEditor + + init(_ parent: SwiftUIRichEditor) { + self.parent = parent + } + + // MARK: - RichEditorDelegate方法 + + public func richEditor(_ editor: RichEditorView, contentDidChange content: String) { + // 更新绑定的HTML内容 + DispatchQueue.main.async { + self.parent.htmlContent = content + self.parent.onContentChange?(content) + } + } + + public func richEditor(_ editor: RichEditorView, heightDidChange height: Int) { + DispatchQueue.main.async { + self.parent.onHeightChange?(height) + } + } + + public func richEditorTookFocus(_ editor: RichEditorView) { + DispatchQueue.main.async { + self.parent.isEditing = true + self.parent.onFocusChange?(true) + } + } + + public func richEditorLostFocus(_ editor: RichEditorView) { + DispatchQueue.main.async { + self.parent.isEditing = false + self.parent.onFocusChange?(false) + } + } + + public func richEditorDidLoad(_ editor: RichEditorView) { + // 编辑器加载完成 + } + + public func richEditor(_ editor: RichEditorView, shouldInteractWith url: URL) -> Bool { + return true + } + + public func richEditor(_ editor: RichEditorView, handle action: String) { + // 处理自定义操作 + } + } +} + +// MARK: - SwiftUI工具栏包装器 + +/// SwiftUI版本的富文本编辑器工具栏 +@available(iOS 13.0, *) +public struct SwiftUIRichEditorToolbar: UIViewRepresentable { + + // MARK: - 属性 + private let options: [RichEditorOption] + private let barTintColor: UIColor? + private weak var editor: RichEditorView? + private let onTextColorChange: (() -> Void)? + private let onBackgroundColorChange: (() -> Void)? + private let onImageInsert: (() -> Void)? + private let onLinkInsert: (() -> Void)? + + // MARK: - 初始化器 + + /// 初始化工具栏 + /// - Parameters: + /// - options: 工具栏选项数组 + /// - barTintColor: 工具栏背景色 + /// - editor: 关联的编辑器 + /// - onTextColorChange: 文字颜色变化回调 + /// - onBackgroundColorChange: 背景颜色变化回调 + /// - onImageInsert: 图片插入回调 + /// - onLinkInsert: 链接插入回调 + public init( + options: [RichEditorOption] = RichEditorDefaultOption.all, + barTintColor: UIColor? = nil, + editor: RichEditorView? = nil, + onTextColorChange: (() -> Void)? = nil, + onBackgroundColorChange: (() -> Void)? = nil, + onImageInsert: (() -> Void)? = nil, + onLinkInsert: (() -> Void)? = nil + ) { + self.options = options + self.barTintColor = barTintColor + self.editor = editor + self.onTextColorChange = onTextColorChange + self.onBackgroundColorChange = onBackgroundColorChange + self.onImageInsert = onImageInsert + self.onLinkInsert = onLinkInsert + } + + // MARK: - UIViewRepresentable实现 + + public func makeCoordinator() -> ToolbarCoordinator { + ToolbarCoordinator(self) + } + + public func makeUIView(context: Context) -> RichEditorToolbar { + let toolbar = RichEditorToolbar() + toolbar.options = options + toolbar.delegate = context.coordinator + toolbar.editor = editor + + if let barTintColor = barTintColor { + toolbar.barTintColor = barTintColor + } + + return toolbar + } + + public func updateUIView(_ uiView: RichEditorToolbar, context: Context) { + uiView.options = options + uiView.editor = editor + + if let barTintColor = barTintColor { + uiView.barTintColor = barTintColor + } + } +} + +// MARK: - 工具栏Coordinator + +@available(iOS 13.0, *) +extension SwiftUIRichEditorToolbar { + public class ToolbarCoordinator: NSObject, RichEditorToolbarDelegate { + var parent: SwiftUIRichEditorToolbar + + init(_ parent: SwiftUIRichEditorToolbar) { + self.parent = parent + } + + // MARK: - RichEditorToolbarDelegate方法 + + public func richEditorToolbarChangeTextColor(_ toolbar: RichEditorToolbar) { + parent.onTextColorChange?() + } + + public func richEditorToolbarChangeBackgroundColor(_ toolbar: RichEditorToolbar) { + parent.onBackgroundColorChange?() + } + + public func richEditorToolbarInsertImage(_ toolbar: RichEditorToolbar) { + parent.onImageInsert?() + } + + public func richEditorToolbarInsertLink(_ toolbar: RichEditorToolbar) { + parent.onLinkInsert?() + } + } +} + diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.pbxproj b/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7205b9d8 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.pbxproj @@ -0,0 +1,600 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 7F7CAD2E2E61FEEA000492B0 /* RichEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 7F7CAD2D2E61FEEA000492B0 /* RichEditorView */; }; + 7F7CAD892E620B47000492B0 /* RichEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 7F7CAD882E620B47000492B0 /* RichEditorView */; }; + 7F9E9BAB2E5B802B004AD074 /* RichEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 7F9E9BAA2E5B802B004AD074 /* RichEditorView */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7F9E9B8D2E5B7FCF004AD074 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7F9E9B772E5B7FCE004AD074 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7F9E9B7E2E5B7FCE004AD074; + remoteInfo = RichEditorViewSwiftUI; + }; + 7F9E9B972E5B7FCF004AD074 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7F9E9B772E5B7FCE004AD074 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7F9E9B7E2E5B7FCE004AD074; + remoteInfo = RichEditorViewSwiftUI; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7F9E9B7F2E5B7FCE004AD074 /* RichEditorViewSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RichEditorViewSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F9E9B8C2E5B7FCF004AD074 /* RichEditorViewSwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RichEditorViewSwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F9E9B962E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RichEditorViewSwiftUIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7F9E9B812E5B7FCE004AD074 /* RichEditorViewSwiftUI */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = RichEditorViewSwiftUI; + sourceTree = ""; + }; + 7F9E9B8F2E5B7FCF004AD074 /* RichEditorViewSwiftUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = RichEditorViewSwiftUITests; + sourceTree = ""; + }; + 7F9E9B992E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = RichEditorViewSwiftUIUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7F9E9B7C2E5B7FCE004AD074 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F7CAD2E2E61FEEA000492B0 /* RichEditorView in Frameworks */, + 7F7CAD892E620B47000492B0 /* RichEditorView in Frameworks */, + 7F9E9BAB2E5B802B004AD074 /* RichEditorView in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B892E5B7FCF004AD074 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B932E5B7FCF004AD074 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7F9E9B762E5B7FCE004AD074 = { + isa = PBXGroup; + children = ( + 7F9E9B812E5B7FCE004AD074 /* RichEditorViewSwiftUI */, + 7F9E9B8F2E5B7FCF004AD074 /* RichEditorViewSwiftUITests */, + 7F9E9B992E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests */, + 7F9E9B802E5B7FCE004AD074 /* Products */, + ); + sourceTree = ""; + }; + 7F9E9B802E5B7FCE004AD074 /* Products */ = { + isa = PBXGroup; + children = ( + 7F9E9B7F2E5B7FCE004AD074 /* RichEditorViewSwiftUI.app */, + 7F9E9B8C2E5B7FCF004AD074 /* RichEditorViewSwiftUITests.xctest */, + 7F9E9B962E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7F9E9B7E2E5B7FCE004AD074 /* RichEditorViewSwiftUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F9E9BA02E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUI" */; + buildPhases = ( + 7F9E9B7B2E5B7FCE004AD074 /* Sources */, + 7F9E9B7C2E5B7FCE004AD074 /* Frameworks */, + 7F9E9B7D2E5B7FCE004AD074 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 7F9E9B812E5B7FCE004AD074 /* RichEditorViewSwiftUI */, + ); + name = RichEditorViewSwiftUI; + packageProductDependencies = ( + 7F9E9BAA2E5B802B004AD074 /* RichEditorView */, + 7F7CAD2D2E61FEEA000492B0 /* RichEditorView */, + 7F7CAD882E620B47000492B0 /* RichEditorView */, + ); + productName = RichEditorViewSwiftUI; + productReference = 7F9E9B7F2E5B7FCE004AD074 /* RichEditorViewSwiftUI.app */; + productType = "com.apple.product-type.application"; + }; + 7F9E9B8B2E5B7FCF004AD074 /* RichEditorViewSwiftUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F9E9BA32E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUITests" */; + buildPhases = ( + 7F9E9B882E5B7FCF004AD074 /* Sources */, + 7F9E9B892E5B7FCF004AD074 /* Frameworks */, + 7F9E9B8A2E5B7FCF004AD074 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7F9E9B8E2E5B7FCF004AD074 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7F9E9B8F2E5B7FCF004AD074 /* RichEditorViewSwiftUITests */, + ); + name = RichEditorViewSwiftUITests; + packageProductDependencies = ( + ); + productName = RichEditorViewSwiftUITests; + productReference = 7F9E9B8C2E5B7FCF004AD074 /* RichEditorViewSwiftUITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7F9E9B952E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F9E9BA62E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUIUITests" */; + buildPhases = ( + 7F9E9B922E5B7FCF004AD074 /* Sources */, + 7F9E9B932E5B7FCF004AD074 /* Frameworks */, + 7F9E9B942E5B7FCF004AD074 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7F9E9B982E5B7FCF004AD074 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7F9E9B992E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests */, + ); + name = RichEditorViewSwiftUIUITests; + packageProductDependencies = ( + ); + productName = RichEditorViewSwiftUIUITests; + productReference = 7F9E9B962E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7F9E9B772E5B7FCE004AD074 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 7F9E9B7E2E5B7FCE004AD074 = { + CreatedOnToolsVersion = 16.4; + }; + 7F9E9B8B2E5B7FCF004AD074 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 7F9E9B7E2E5B7FCE004AD074; + }; + 7F9E9B952E5B7FCF004AD074 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 7F9E9B7E2E5B7FCE004AD074; + }; + }; + }; + buildConfigurationList = 7F9E9B7A2E5B7FCE004AD074 /* Build configuration list for PBXProject "RichEditorViewSwiftUI" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7F9E9B762E5B7FCE004AD074; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 7F7CAD872E620B47000492B0 /* XCLocalSwiftPackageReference "../../RichEditorView" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 7F9E9B802E5B7FCE004AD074 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7F9E9B7E2E5B7FCE004AD074 /* RichEditorViewSwiftUI */, + 7F9E9B8B2E5B7FCF004AD074 /* RichEditorViewSwiftUITests */, + 7F9E9B952E5B7FCF004AD074 /* RichEditorViewSwiftUIUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7F9E9B7D2E5B7FCE004AD074 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B8A2E5B7FCF004AD074 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B942E5B7FCF004AD074 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7F9E9B7B2E5B7FCE004AD074 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B882E5B7FCF004AD074 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7F9E9B922E5B7FCF004AD074 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7F9E9B8E2E5B7FCF004AD074 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7F9E9B7E2E5B7FCE004AD074 /* RichEditorViewSwiftUI */; + targetProxy = 7F9E9B8D2E5B7FCF004AD074 /* PBXContainerItemProxy */; + }; + 7F9E9B982E5B7FCF004AD074 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7F9E9B7E2E5B7FCE004AD074 /* RichEditorViewSwiftUI */; + targetProxy = 7F9E9B972E5B7FCF004AD074 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7F9E9B9E2E5B7FCF004AD074 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7F9E9B9F2E5B7FCF004AD074 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7F9E9BA12E5B7FCF004AD074 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7F9E9BA22E5B7FCF004AD074 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7F9E9BA42E5B7FCF004AD074 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RichEditorViewSwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RichEditorViewSwiftUI"; + }; + name = Debug; + }; + 7F9E9BA52E5B7FCF004AD074 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RichEditorViewSwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/RichEditorViewSwiftUI"; + }; + name = Release; + }; + 7F9E9BA72E5B7FCF004AD074 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUIUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RichEditorViewSwiftUI; + }; + name = Debug; + }; + 7F9E9BA82E5B7FCF004AD074 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T5WWP2Z4XN; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ites.easumerebuild4.RichEditorViewSwiftUIUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RichEditorViewSwiftUI; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7F9E9B7A2E5B7FCE004AD074 /* Build configuration list for PBXProject "RichEditorViewSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F9E9B9E2E5B7FCF004AD074 /* Debug */, + 7F9E9B9F2E5B7FCF004AD074 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F9E9BA02E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F9E9BA12E5B7FCF004AD074 /* Debug */, + 7F9E9BA22E5B7FCF004AD074 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F9E9BA32E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F9E9BA42E5B7FCF004AD074 /* Debug */, + 7F9E9BA52E5B7FCF004AD074 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F9E9BA62E5B7FCF004AD074 /* Build configuration list for PBXNativeTarget "RichEditorViewSwiftUIUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F9E9BA72E5B7FCF004AD074 /* Debug */, + 7F9E9BA82E5B7FCF004AD074 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 7F7CAD872E620B47000492B0 /* XCLocalSwiftPackageReference "../../RichEditorView" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../RichEditorView; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7F7CAD2D2E61FEEA000492B0 /* RichEditorView */ = { + isa = XCSwiftPackageProductDependency; + productName = RichEditorView; + }; + 7F7CAD882E620B47000492B0 /* RichEditorView */ = { + isa = XCSwiftPackageProductDependency; + productName = RichEditorView; + }; + 7F9E9BAA2E5B802B004AD074 /* RichEditorView */ = { + isa = XCSwiftPackageProductDependency; + productName = RichEditorView; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 7F9E9B772E5B7FCE004AD074 /* Project object */; +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/Contents.json b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI/ContentView.swift b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/ContentView.swift new file mode 100644 index 00000000..8d67ec41 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/ContentView.swift @@ -0,0 +1,176 @@ +// +// ContentView.swift +// RichEditorViewSwiftUI +// +// Created by yunning you on 2025/8/25. +// + +import SwiftUI +import RichEditorView + +struct ContentView: View { + + // Rich text editor state management + @StateObject private var editorState = RichEditorState( + htmlContent: """ +
    +
  1. This is the first line for testing styles.
  2. +
  3. Bold this line.
  4. +
  5. Make this line italic.
  6. +
  7. Underline this one.
  8. +
  9. Apply a strikethrough here.
  10. +
  11. This is for subscript test: H2O.
  12. +
  13. And this is for superscript test: E=mc2.
  14. +
  15. Let's test text color on this sentence.
  16. +
  17. Now, let's try a background color.
  18. +
  19. Heading 1 will be applied here.

  20. +
  21. This should be Heading 2.

  22. +
  23. And here comes Heading 3.

  24. +
  25. Indent this paragraph.

  26. +
  27. This is a standard line.
  28. +
  29. This is the fifteenth item in our list.
  30. +
  31. A slightly longer sentence to check wrapping and alignment.
  32. +
  33. Left align this text.

  34. +
  35. Center align this text.

  36. +
  37. Right align this text.

  38. +
  39. Let's see how a hyperlink looks.
  40. +
  41. This line contains some special characters: @#$%^&*().
  42. +
  43. A sentence with multiple formats.
  44. +
  45. The twenty-third line to test scrolling behavior.
  46. +
  47. Almost there, just one more line to go.
  48. +
  49. This is the final line for testing!
  50. +
+ """ + ) + + // 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() +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUI/RichEditorViewSwiftUIApp.swift b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/RichEditorViewSwiftUIApp.swift new file mode 100644 index 00000000..6dd797aa --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUI/RichEditorViewSwiftUIApp.swift @@ -0,0 +1,17 @@ +// +// RichEditorViewSwiftUIApp.swift +// RichEditorViewSwiftUI +// +// Created by yunning you on 2025/8/25. +// + +import SwiftUI + +@main +struct RichEditorViewSwiftUIApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUITests/RichEditorViewSwiftUITests.swift b/RichEditorViewSwiftUI/RichEditorViewSwiftUITests/RichEditorViewSwiftUITests.swift new file mode 100644 index 00000000..a9a8b01f --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUITests/RichEditorViewSwiftUITests.swift @@ -0,0 +1,36 @@ +// +// RichEditorViewSwiftUITests.swift +// RichEditorViewSwiftUITests +// +// Created by yunning you on 2025/8/25. +// + +import XCTest +@testable import RichEditorViewSwiftUI + +final class RichEditorViewSwiftUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITests.swift b/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITests.swift new file mode 100644 index 00000000..f71da156 --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITests.swift @@ -0,0 +1,41 @@ +// +// RichEditorViewSwiftUIUITests.swift +// RichEditorViewSwiftUIUITests +// +// Created by yunning you on 2025/8/25. +// + +import XCTest + +final class RichEditorViewSwiftUIUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITestsLaunchTests.swift b/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITestsLaunchTests.swift new file mode 100644 index 00000000..6d31ff2c --- /dev/null +++ b/RichEditorViewSwiftUI/RichEditorViewSwiftUIUITests/RichEditorViewSwiftUIUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// RichEditorViewSwiftUIUITestsLaunchTests.swift +// RichEditorViewSwiftUIUITests +// +// Created by yunning you on 2025/8/25. +// + +import XCTest + +final class RichEditorViewSwiftUIUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/art/SwiftUI.jpg b/art/SwiftUI.jpg new file mode 100644 index 00000000..11174fb3 Binary files /dev/null and b/art/SwiftUI.jpg differ