├── Example ├── Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Example.entitlements │ ├── ExampleApp.swift │ ├── Examples │ │ ├── ExampleView.swift │ │ ├── OnChangeView.swift │ │ ├── PlaceholderView.swift │ │ ├── FontExampleView.swift │ │ ├── TextColorView.swift │ │ ├── TextDiffView.swift │ │ └── MutableAttributedStringExampleView.swift │ └── ContentView.swift └── Example.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── renovate.json ├── .github └── FUNDING.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── TextEditorPlusTests │ └── TextEditorPlusTests.swift ├── Package.resolved ├── Sources └── TextEditorPlus │ ├── Settings │ ├── Font.swift │ └── TextSetting.swift │ ├── Extensions │ └── Coordinator.swift │ └── TextEditorPlus.swift ├── Package.swift ├── LICENSE ├── .gitignore └── README.md /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: jaywcjlove 2 | buy_me_a_coffee: jaywcjlove 3 | custom: ["https://www.paypal.me/kennyiseeyou", "https://jaywcjlove.github.io/#/sponsor"] 4 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/TextEditorPlusTests/TextEditorPlusTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TextEditorPlus 3 | 4 | final class TextEditorPlusTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | #if os(macOS) 17 | .windowToolbarStyle(UnifiedCompactWindowToolbarStyle()) 18 | // .windowStyle(HiddenTitleBarWindowStyle()) 19 | #endif 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "663ec50e92d2a184eca44487f8f55176c073ebcfbd537da1a76b7d02e1f9ad19", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-docc-plugin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-docc-plugin", 8 | "state" : { 9 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 10 | "version" : "1.4.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-docc-symbolkit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 17 | "state" : { 18 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 19 | "version" : "1.0.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Example/Example/Examples/ExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct ExampleView: View { 12 | @State var text = """ 13 | Hello World 14 | """ 15 | @State var isEditable = true 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 0) { 18 | Button(isEditable ? "Disable Editing" : "Edit") { 19 | isEditable.toggle() 20 | } 21 | .padding() 22 | 23 | TextEditorPlus(text: $text) 24 | .textSetting(isEditable, for: .isEditable) 25 | } 26 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 27 | } 28 | } 29 | 30 | #Preview { 31 | ExampleView() 32 | } 33 | -------------------------------------------------------------------------------- /Example/Example/Examples/OnChangeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnChangeView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/27. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct OnChangeView: View { 12 | @State var text = """ 13 | Hello World 14 | """ 15 | @State var isEditable = true 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 0) { 18 | Button(isEditable ? "Disable Editing" : "Edit") { 19 | isEditable.toggle() 20 | } 21 | .padding() 22 | 23 | TextEditorPlus(text: $text) 24 | .textSetting(isEditable, for: .isEditable) 25 | } 26 | .onChange(of: text, initial: true, { old, val in 27 | print("val: \(val)") 28 | }) 29 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 30 | } 31 | } 32 | 33 | #Preview { 34 | OnChangeView() 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TextEditorPlus/Settings/Font.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(OSX) 11 | import AppKit 12 | public typealias FontHelper = NSFont 13 | #elseif os(iOS) 14 | import UIKit 15 | public typealias FontHelper = UIFont 16 | #endif 17 | 18 | @propertyWrapper 19 | struct Font { 20 | var value: FontHelper 21 | 22 | init(wrappedValue: FontHelper) { 23 | self.value = wrappedValue 24 | } 25 | 26 | var wrappedValue: FontHelper { 27 | get { value } 28 | set { value = newValue } 29 | } 30 | } 31 | 32 | 33 | extension TextEditorPlus { 34 | /// Set font size 35 | /// 36 | /// ```swift 37 | /// TextEditorPlus(text: $text) 38 | /// .font(NSFont(name: "pencontrol", size: 12)!) 39 | /// .font(.systemFont(ofSize: 24, weight: .regular)) 40 | /// ``` 41 | public func font(_ value: FontHelper) -> Self { 42 | var view = self 43 | view.font = value 44 | return view 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TextEditorPlus", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v11) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "TextEditorPlus", 16 | targets: ["TextEditorPlus"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package, defining a module or a test suite. 23 | // Targets can depend on other targets in this package and products from dependencies. 24 | .target( 25 | name: "TextEditorPlus"), 26 | .testTarget( 27 | name: "TextEditorPlusTests", 28 | dependencies: ["TextEditorPlus"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "a12562b6ce2bd67ab8f2b704d14197ccd192175e64c7d46a2f3f154ff22ea2c4", 3 | "pins" : [ 4 | { 5 | "identity" : "sdifft", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/wzxha/Sdifft.git", 8 | "state" : { 9 | "revision" : "6c39e659b07c0530c67721e00de023afd57aea00", 10 | "version" : "2.1.0" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-docc-plugin", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-docc-plugin", 17 | "state" : { 18 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", 19 | "version" : "1.3.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-docc-symbolkit", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-docc-symbolkit", 26 | "state" : { 27 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 28 | "version" : "1.0.0" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kenny Wang(小弟调调™) (https://github.com/jaywcjlove) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/Example/Examples/PlaceholderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaceholderView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/26. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct PlaceholderView: View { 12 | @State var text = "" 13 | @State var isEditable = true 14 | @State var fontSize: String = "32" 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 0) { 17 | HStack { 18 | Button(isEditable ? "Disable Editing" : "Edit") { 19 | isEditable.toggle() 20 | } 21 | 22 | Picker(selection: $fontSize, content: { 23 | Text("12").tag("12") 24 | Text("24").tag("24") 25 | Text("32").tag("32") 26 | }, label: { 27 | Text("Size") 28 | }) 29 | .frame(width: 86) 30 | } 31 | .padding() 32 | 33 | TextEditorPlus(text: $text) 34 | .font(.systemFont(ofSize: CGFloat(Float(fontSize)!), weight: .regular)) 35 | .textSetting(isEditable, for: .isEditable) 36 | .textSetting("Test placeholder string", for: .placeholderString) 37 | } 38 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 39 | } 40 | } 41 | 42 | #Preview { 43 | PlaceholderView() 44 | } 45 | -------------------------------------------------------------------------------- /Example/Example/Examples/FontExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontExampleView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct FontExampleView: View { 12 | @State var text = """ 13 | Hello World 14 | """ 15 | @State var fontSize: String = "32" 16 | @State var insetPadding: String = "18" 17 | var body: some View { 18 | VStack(alignment: .leading, spacing: 0) { 19 | HStack { 20 | Picker(selection: $insetPadding, content: { 21 | Text("18").tag("18") 22 | Text("24").tag("24") 23 | Text("32").tag("32") 24 | Text("42").tag("42") 25 | }, label: { 26 | Text("Inset Padding") 27 | }) 28 | .frame(width: 137) 29 | 30 | Picker(selection: $fontSize, content: { 31 | Text("12").tag("12") 32 | Text("24").tag("24") 33 | Text("32").tag("32") 34 | }, label: { 35 | Text("Size") 36 | }) 37 | .frame(width: 86) 38 | } 39 | .padding() 40 | 41 | TextEditorPlus(text: $text) 42 | .font(.systemFont(ofSize: CGFloat(Float(fontSize)!), weight: .regular)) 43 | .textSetting(CGFloat(Float(insetPadding)!), for: .insetPadding) 44 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 45 | } 46 | } 47 | } 48 | 49 | #Preview { 50 | FontExampleView() 51 | } 52 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct SideBar: Identifiable, Hashable, Equatable { 12 | static func == (lhs: SideBar, rhs: SideBar) -> Bool { 13 | lhs.id == rhs.id 14 | } 15 | func hash(into hasher: inout Hasher) { 16 | hasher.combine(id) 17 | } 18 | let id = UUID() 19 | var name: String 20 | var view: AnyView 21 | } 22 | 23 | struct ContentView: View { 24 | @State private var examples = [ 25 | SideBar(name: "Text Diff Test", view: AnyView(TextDiffView())), 26 | SideBar(name: "Text Color Test", view: AnyView(TextColorView())), 27 | SideBar(name: "Change Event Test", view: AnyView(OnChangeView())), 28 | SideBar(name: "Placeholder Example", view: AnyView(PlaceholderView())), 29 | SideBar(name: "Mutable Attributed String", view: AnyView(MutableAttributedStringExampleView())), 30 | SideBar(name: "Example", view: AnyView(ExampleView())), 31 | SideBar(name: "Font Example", view: AnyView(FontExampleView())), 32 | ] 33 | @State private var selected: SideBar? 34 | var body: some View { 35 | NavigationSplitView { 36 | List(examples, selection: $selected) { team in 37 | Text(team.name).tag(team) 38 | } 39 | .navigationSplitViewColumnWidth(180) 40 | } detail: { 41 | selected?.view ?? AnyView(EmptyView()) 42 | } 43 | .navigationSplitViewStyle(.prominentDetail) 44 | .onAppear() { 45 | selected = examples.first 46 | } 47 | } 48 | } 49 | 50 | #Preview { 51 | ContentView() 52 | } 53 | -------------------------------------------------------------------------------- /Example/Example/Examples/TextColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextColorView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/6/4. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | struct TextColorView: View { 12 | @State var text = """ 13 | Hello World 14 | """ 15 | @State var color: Color = .red 16 | var body: some View { 17 | VStack(alignment: .leading, spacing: 0) { 18 | ColorPicker(selection: $color, supportsOpacity: true, label: { 19 | 20 | }) 21 | .controlSize(.small) 22 | .labelsHidden() 23 | .padding() 24 | 25 | TextEditorPlus(text: $text) 26 | .foregroundColor(.red) 27 | .background(Color.blue) 28 | Divider() 29 | TextEditor(text: $text) 30 | .foregroundColor(.red) 31 | .background(.background) 32 | .background( 33 | HStack(alignment: .top) { 34 | text.isEmpty ? Text("placeholder") : Text("") 35 | } 36 | .foregroundColor(Color.primary.opacity(0.25)) 37 | .padding(EdgeInsets(top: 28, leading: 32, bottom: 7, trailing: 0)) 38 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 39 | .border(.red) 40 | ) 41 | } 42 | .onChange(of: text, initial: true, { old, val in 43 | print("val: \(val)") 44 | }) 45 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 46 | } 47 | } 48 | 49 | #Preview { 50 | TextColorView() 51 | } 52 | 53 | #if os(OSX) 54 | extension NSTextView { 55 | open override var frame: CGRect { 56 | didSet { 57 | backgroundColor = .clear //< NSMutableAttributedString? = { _ in nil } 34 | } 35 | 36 | extension EnvironmentValues { 37 | /// Set whether it is editable. 38 | var textViewIsEditable: Bool { 39 | get { self[TextViewIsEditable.self] } 40 | set { self[TextViewIsEditable.self] = newValue } 41 | } 42 | /// Set padding insets. 43 | var textViewInsetPadding: CGFloat { 44 | get { self[TextViewInsetPadding.self] } 45 | set { self[TextViewInsetPadding.self] = newValue } 46 | } 47 | var textViewBackgroundColor: ViewColor? { 48 | get { self[TextViewBackgroundColor.self] } 49 | set { self[TextViewBackgroundColor.self] = newValue } 50 | } 51 | var textViewAttributedString: (NSMutableAttributedString) -> NSMutableAttributedString? { 52 | get { self[TextViewAttributedString.self] } 53 | set { self[TextViewAttributedString.self] = newValue } 54 | } 55 | var textViewPlaceholderString: String? { 56 | get { self[TextViewPlaceholderString.self] } 57 | set { self[TextViewPlaceholderString.self] = newValue } 58 | } 59 | } 60 | 61 | public enum TextViewComponent { 62 | /// Is the editor editable? 63 | case isEditable 64 | /// Set editor padding 65 | case insetPadding 66 | /// Set the editor background color. 67 | case backgroundColor 68 | /// Set editor placeholder 69 | case placeholderString 70 | } 71 | 72 | @available(iOS 13.0, macOS 10.15, *) 73 | public extension View { 74 | /// Manipulate attributed strings with attributes such as visual styles, hyperlinks, or accessibility data for portions of the text. 75 | /// 76 | /// ```swift 77 | /// TextEditorPlus(text: $text) 78 | /// .textSetting(isEditable, for: .isEditable) 79 | /// .textViewAttributedString(action: { val in 80 | /// let style = NSMutableParagraphStyle() 81 | /// style.lineSpacing = 5 82 | /// style.lineHeightMultiple = 1.2 83 | /// val.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: val.length)) 84 | /// return val 85 | /// }) 86 | /// ```` 87 | @ViewBuilder func textViewAttributedString(action: @escaping (NSMutableAttributedString) -> NSMutableAttributedString?) -> some View { 88 | environment(\.textViewAttributedString, action) 89 | } 90 | /// Sets the tint color for specific MarkdownView component. 91 | /// 92 | /// ```swift 93 | /// TextEditorPlus(text: $text) 94 | /// .textSetting(isEditable, for: .isEditable) 95 | /// .textSetting(25, for: .insetPadding) 96 | /// ``` 97 | /// 98 | /// - Parameters: 99 | /// - value: The value of the component attribute. 100 | /// - component: Specify the component's attribute. 101 | @ViewBuilder func textSetting(_ value: T, for component: TextViewComponent) -> some View { 102 | switch component { 103 | case .isEditable: 104 | environment(\.textViewIsEditable, value as! Bool) 105 | case .insetPadding: 106 | environment(\.textViewInsetPadding, value as! CGFloat) 107 | case .backgroundColor: 108 | environment(\.textViewBackgroundColor, value as! ViewColor?) 109 | case .placeholderString: 110 | environment(\.textViewPlaceholderString, value as! String?) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/TextEditorPlus/Extensions/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 王楚江 on 2024/3/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Coordinator 11 | @available(iOS 13.0, macOS 10.15, *) 12 | extension TextEditorPlus { 13 | #if os(iOS) 14 | public class Coordinator: NSObject, UITextViewDelegate { 15 | var parent: TextEditorPlus 16 | var selectedRanges: [NSValue] = [] 17 | private var debounceTimer: Timer? 18 | 19 | init(_ parent: TextEditorPlus) { 20 | self.parent = parent 21 | } 22 | 23 | public func textViewDidBeginEditing(_ textView: UITextView) { 24 | if parent.isAttributedTextMode { 25 | if let attributedText = parent.attributedText { 26 | attributedText.setAttributedString(textView.attributedText) 27 | } 28 | } else { 29 | self.parent.text = textView.text 30 | } 31 | self.selectedRanges = textView.selectedTextRange != nil ? [NSValue(range: textView.selectedRange)] : [] 32 | } 33 | 34 | public func textViewDidChange(_ textView: UITextView) { 35 | // 对于大文本,使用防抖机制减少频繁更新 36 | debounceTimer?.invalidate() 37 | debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { [weak self] _ in 38 | DispatchQueue.main.async { 39 | if let parent = self?.parent { 40 | if parent.isAttributedTextMode { 41 | if let attributedText = parent.attributedText { 42 | attributedText.setAttributedString(textView.attributedText) 43 | } 44 | } else { 45 | parent.text = textView.text 46 | } 47 | } 48 | self?.selectedRanges = textView.selectedTextRange != nil ? [NSValue(range: textView.selectedRange)] : [] 49 | } 50 | } 51 | } 52 | 53 | public func textViewDidEndEditing(_ textView: UITextView) { 54 | debounceTimer?.invalidate() 55 | if parent.isAttributedTextMode { 56 | if let attributedText = parent.attributedText { 57 | attributedText.setAttributedString(textView.attributedText) 58 | } 59 | } else { 60 | self.parent.text = textView.text 61 | } 62 | self.selectedRanges = textView.selectedTextRange != nil ? [NSValue(range: textView.selectedRange)] : [] 63 | } 64 | } 65 | #endif 66 | #if os(OSX) 67 | public class Coordinator: NSObject, NSTextViewDelegate { 68 | var parent: TextEditorPlus 69 | var selectedRanges: [NSValue] = [] 70 | private var debounceTimer: Timer? 71 | 72 | init(_ parent: TextEditorPlus) { 73 | self.parent = parent 74 | } 75 | public func textDidBeginEditing(_ notification: Notification) { 76 | guard let textView = notification.object as? NSTextView else { 77 | return 78 | } 79 | if parent.isAttributedTextMode { 80 | if let attributedText = parent.attributedText, 81 | let textStorage = textView.textStorage { 82 | attributedText.setAttributedString(textStorage) 83 | } 84 | } else { 85 | self.parent.text = textView.string 86 | } 87 | self.selectedRanges = textView.selectedRanges 88 | } 89 | public func textDidChange(_ notification: Notification) { 90 | guard let textView = notification.object as? NSTextView else { 91 | return 92 | } 93 | // 对于大文本,使用防抖机制减少频繁更新 94 | debounceTimer?.invalidate() 95 | debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { [weak self] _ in 96 | DispatchQueue.main.async { 97 | if let parent = self?.parent { 98 | if parent.isAttributedTextMode { 99 | if let attributedText = parent.attributedText, 100 | let textStorage = textView.textStorage { 101 | attributedText.setAttributedString(textStorage) 102 | } 103 | } else { 104 | parent.text = textView.string 105 | } 106 | } 107 | self?.selectedRanges = textView.selectedRanges 108 | } 109 | } 110 | } 111 | 112 | public func textDidEndEditing(_ notification: Notification) { 113 | guard let textView = notification.object as? NSTextView else { 114 | return 115 | } 116 | debounceTimer?.invalidate() 117 | if parent.isAttributedTextMode { 118 | if let attributedText = parent.attributedText, 119 | let textStorage = textView.textStorage { 120 | attributedText.setAttributedString(textStorage) 121 | } 122 | } else { 123 | self.parent.text = textView.string 124 | } 125 | self.selectedRanges = textView.selectedRanges 126 | } 127 | } 128 | #endif 129 | } 130 | -------------------------------------------------------------------------------- /Example/Example/Examples/TextDiffView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffView.swift 3 | // Example 4 | // 5 | // Created by wong on 8/17/25. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | import Sdifft 11 | 12 | extension LocalizedStringKey { 13 | func localizedString(locale: Locale) -> String { 14 | let mirror = Mirror(reflecting: self) 15 | let key = mirror.children.first { $0.label == "key" }?.value as? String ?? "" 16 | 17 | let languageCode = locale.identifier 18 | let path = Bundle.main.path(forResource: languageCode, ofType: "lproj") ?? "" 19 | let bundle = Bundle(path: path) ?? .main 20 | 21 | return NSLocalizedString(key, bundle: bundle, comment: "") 22 | } 23 | } 24 | 25 | // MARK: 全局状态 26 | class TextDiff: ObservableObject { 27 | @Published var source: String = "Hello World!" 28 | @Published var target: String = "Hello DevHub!" 29 | @Published var attributedString: NSMutableAttributedString = .init() 30 | } 31 | 32 | struct TextDiffView: View { 33 | @StateObject var vm = TextDiff() 34 | @Environment(\.colorScheme) var colorScheme 35 | @Environment(\.locale) var locale 36 | 37 | @FocusState var input: InputFocused? 38 | @State var loading: Bool = false 39 | private var text = NSAttributedString() 40 | enum InputFocused { 41 | case text,output 42 | } 43 | 44 | var body: some View { 45 | VSplitView { 46 | HSplitView { 47 | let prompt = LocalizedStringKey("Paste your text here").localizedString(locale: locale) 48 | TextEditorPlus(text: $vm.source) 49 | .font(.systemFont(ofSize: 14, weight: .regular)) 50 | .textSetting(CGFloat(5), for: .insetPadding) 51 | .textSetting(prompt, for: .placeholderString) 52 | .frame(minHeight: 120) 53 | .focused($input, equals: .text) 54 | .onChange(of: vm.source, initial: true) { oldValue, newValue in 55 | if input == .text { 56 | update() 57 | } 58 | } 59 | TextEditorPlus(text: $vm.target) 60 | .font(.systemFont(ofSize: 14, weight: .regular)) 61 | .textSetting(CGFloat(10), for: .insetPadding) 62 | .textSetting(prompt, for: .placeholderString) 63 | .frame(minHeight: 120) 64 | .focused($input, equals: .output) 65 | .onChange(of: vm.target, initial: true) { oldValue, newValue in 66 | if input == .output { 67 | update() 68 | } 69 | } 70 | 71 | } 72 | ZStack { 73 | TextEditorPlus(text: $vm.attributedString).frame(minHeight: 120) 74 | if loading == true { 75 | ProgressView().controlSize(.mini) 76 | } 77 | } 78 | } 79 | .frame(maxWidth: .infinity, maxHeight: .infinity) 80 | .onAppear() { 81 | update() 82 | } 83 | } 84 | func update() { 85 | loading = true 86 | DispatchQueue.global(qos: .userInitiated).async { 87 | guard let result = updateAttributedString(source: vm.source, target: vm.target) else { 88 | loading = false 89 | return 90 | } 91 | DispatchQueue.main.async { 92 | vm.attributedString = result as! NSMutableAttributedString 93 | loading = false 94 | } 95 | } 96 | } 97 | func updateAttributedString(source: String, target: String) -> NSAttributedString? { 98 | // 创建一个简单的测试文本,确保有内容 99 | let testText = "" 100 | let basicAttributedString = NSMutableAttributedString(string: testText) 101 | 102 | // 设置基本样式 103 | let nsColor = colorScheme == .dark ? NSColor.white : NSColor.black 104 | let font: NSFont = .systemFont(ofSize: 14, weight: .regular) 105 | 106 | let fullRange = NSRange(location: 0, length: basicAttributedString.length) 107 | basicAttributedString.addAttribute(.foregroundColor, value: nsColor, range: fullRange) 108 | basicAttributedString.addAttribute(.font, value: font, range: fullRange) 109 | 110 | // 尝试使用 Sdifft 进行差异比较(如果可用) 111 | let diffAttributes = DiffAttributes( 112 | insert: [.backgroundColor: NSColor.systemGreen.withAlphaComponent(0.3)], 113 | delete: [.backgroundColor: NSColor.systemRed.withAlphaComponent(0.3)], 114 | same: [.backgroundColor: NSColor.clear] 115 | ) 116 | 117 | let diffResult = NSMutableAttributedString(source: source, target: target, attributes: diffAttributes) 118 | var _ = print("diffResult", diffResult.length) 119 | if diffResult.length > 0 { 120 | // 为 diff 结果添加基本样式 121 | let diffRange = NSRange(location: 0, length: diffResult.length) 122 | diffResult.addAttribute(.foregroundColor, value: nsColor, range: diffRange) 123 | diffResult.addAttribute(.font, value: font, range: diffRange) 124 | 125 | // 添加标题 126 | let diffWithTitle = NSMutableAttributedString() 127 | diffWithTitle.append(diffResult) 128 | 129 | print("✅ Using Sdifft diff result with length: \(diffWithTitle.length)") 130 | return diffWithTitle 131 | } 132 | return basicAttributedString 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Example/Example/Examples/MutableAttributedStringExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableAttributedStringExampleView.swift 3 | // Example 4 | // 5 | // Created by 王楚江 on 2024/3/26. 6 | // 7 | 8 | import SwiftUI 9 | import TextEditorPlus 10 | 11 | #if os(OSX) 12 | import AppKit 13 | public typealias ViewColor = NSColor 14 | #elseif os(iOS) 15 | import UIKit 16 | public typealias ViewColor = UIColor 17 | #endif 18 | 19 | struct MutableAttributedStringExampleView: View { 20 | @State var text = """ 21 | Hello World 22 | """ 23 | @State var attributedText = NSMutableAttributedString(string: """ 24 | This is a NSMutableAttributedString example. 25 | You can edit this text directly. 26 | The formatting will be preserved! 27 | """) 28 | @State var pattern: String = "[a-z]*" 29 | @State var isEditable = true 30 | @State var useAttributedString = false 31 | 32 | var body: some View { 33 | VStack(alignment: .leading, spacing: 0) { 34 | HStack { 35 | TextField("Pattern", text: $pattern).labelsHidden() 36 | Button(isEditable ? "Disable Editing" : "Edit") { 37 | isEditable.toggle() 38 | } 39 | Button(useAttributedString ? "Use String" : "Use AttributedString") { 40 | useAttributedString.toggle() 41 | } 42 | } 43 | .padding() 44 | 45 | if useAttributedString { 46 | // 使用 NSMutableAttributedString 绑定 47 | TextEditorPlus(text: $attributedText) 48 | .textSetting(isEditable, for: .isEditable) 49 | .onAppear { 50 | setupAttributedText() 51 | } 52 | } else { 53 | // 使用 String 绑定 54 | TextEditorPlus(text: $text) 55 | .textSetting(isEditable, for: .isEditable) 56 | .textViewAttributedString(action: { val in 57 | if isValidRegex(pattern) { 58 | val.matchText(pattern: pattern) 59 | let style = NSMutableParagraphStyle() 60 | style.lineSpacing = 5 61 | style.lineHeightMultiple = 1.2 62 | 63 | val.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: val.length)) 64 | return val 65 | } 66 | return nil 67 | }) 68 | } 69 | } 70 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 71 | } 72 | 73 | func setupAttributedText() { 74 | let fullRange = NSRange(location: 0, length: attributedText.length) 75 | 76 | // 设置基础字体 77 | #if os(iOS) 78 | attributedText.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: fullRange) 79 | #else 80 | attributedText.addAttribute(.font, value: NSFont.systemFont(ofSize: 16), range: fullRange) 81 | #endif 82 | 83 | // 高亮 "NSMutableAttributedString" 文字 84 | let highlightText = "NSMutableAttributedString" 85 | let highlightRange = (attributedText.string as NSString).range(of: highlightText) 86 | if highlightRange.location != NSNotFound { 87 | attributedText.addAttribute(.backgroundColor, value: ViewColor.systemBlue, range: highlightRange) 88 | attributedText.addAttribute(.foregroundColor, value: ViewColor.white, range: highlightRange) 89 | } 90 | 91 | // 设置段落样式 92 | let paragraphStyle = NSMutableParagraphStyle() 93 | paragraphStyle.lineSpacing = 3 94 | paragraphStyle.paragraphSpacing = 8 95 | attributedText.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange) 96 | } 97 | func isValidRegex(_ pattern: String) -> Bool { 98 | do { 99 | // 尝试编译正则表达式 100 | let _ = try NSRegularExpression(pattern: pattern, options: []) 101 | return true // 如果编译成功,则正则表达式有效 102 | } catch { 103 | // 如果编译失败,则正则表达式无效 104 | return false 105 | } 106 | } 107 | } 108 | 109 | extension NSMutableAttributedString { 110 | func setLineHeight(_ lineHeight: CGFloat) { 111 | let paragraphStyle = NSMutableParagraphStyle() 112 | paragraphStyle.minimumLineHeight = lineHeight // 设置行高 113 | paragraphStyle.maximumLineHeight = lineHeight // 设置行高 114 | paragraphStyle.lineHeightMultiple = lineHeight // 设置行高 115 | 116 | self.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: self.length)) 117 | } 118 | // 匹配文本 119 | func matchText(pattern: String) { 120 | do { 121 | let regex = try NSRegularExpression(pattern: pattern, options: []) 122 | let range = NSRange(location: 0, length: self.length) 123 | let matches = regex.matches(in: self.string, options: [], range: range) 124 | for (index, match) in matches.enumerated() { 125 | let color: ViewColor 126 | if index % 2 == 0 { // 如果索引是偶数 127 | color = ViewColor(red: 0.608, green: 0.231, blue: 0.780, alpha: 1) // 设置第一种背景颜色 128 | } else { 129 | color = ViewColor(red: 0.239, green: 0.494, blue: 0.780, alpha: 1) // 设置第二种背景颜色 130 | } 131 | self.addAttribute(.foregroundColor, value: ViewColor.white, range: match.range) 132 | self.addAttribute(.backgroundColor, value: color, range: match.range) 133 | } 134 | 135 | } catch { 136 | print("Error highlighting parentheses: \(error.localizedDescription)") 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Using my app is also a way to support me: 3 |
4 | Deskmark 5 | Keyzer 6 | Vidwall Hub 7 | VidCrop 8 | Vidwall 9 | Mousio Hint 10 | Mousio 11 | Musicer 12 | Audioer 13 | FileSentinel 14 | FocusCursor 15 | Videoer 16 | KeyClicker 17 | DayBar 18 | Iconed 19 | Mousio 20 | Quick RSS 21 | Quick RSS 22 | Web Serve 23 | Copybook Generator 24 | DevTutor for SwiftUI 25 | RegexMate 26 | Time Passage 27 | Iconize Folder 28 | Textsound Saver 29 | Create Custom Symbols 30 | DevHub 31 | Resume Revise 32 | Palette Genius 33 | Symbol Scribe 34 |
35 |
36 | 37 | SwiftUI TextEditorPlus 38 | === 39 | 40 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 41 | [![SwiftUI Support](https://shields.io/badge/SwiftUI-macOS%20v11%20%7C%20iOS%20v13-green?logo=Swift&style=flat)](https://swiftpackageindex.com/jaywcjlove/swiftui-texteditor) 42 | 43 | An enhanced version similar to `TextEditor`, aimed at maintaining consistency in its usage across iOS and macOS platforms. 44 | 45 | Welcome to download [DevTutor](https://apps.apple.com/app/devtutor/id6471227008), a cheat sheet app designed to help developers quickly build excellent applications using SwiftUI. 46 | 47 |

48 | DevTutor for SwiftUI AppStore 49 | 50 |

51 | 52 | ## Installation 53 | 54 | You can add MarkdownUI to an Xcode project by adding it as a package dependency. 55 | 56 | 1. From the File menu, select Add Packages… 57 | 2. Enter https://github.com/jaywcjlove/swiftui-texteditor the Search or Enter Package URL search field 58 | 3. Link `Markdown` to your application target 59 | 60 | Or add the following to `Package.swift`: 61 | 62 | ```swift 63 | .package(url: "https://github.com/jaywcjlove/swiftui-texteditor", from: "1.0.0") 64 | ``` 65 | 66 | Or [add the package in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). 67 | 68 | ## Usage 69 | 70 | ```swift 71 | import TextEditorPlus 72 | 73 | struct ContentView: View { 74 | @State var text = """ 75 | Hello World 76 | """ 77 | @State var isEditable = true 78 | var body: some View { 79 | TextEditorPlus(text: $text) 80 | .textSetting(isEditable, for: .isEditable) 81 | } 82 | } 83 | ``` 84 | 85 | Set text weight and size: 86 | 87 | ```swift 88 | TextEditorPlus(text: $text) 89 | .font(.systemFont(ofSize: 24, weight: .regular)) 90 | ``` 91 | 92 | Set editor padding: 93 | 94 | ```swift 95 | TextEditorPlus(text: $text) 96 | .textSetting(23, for: .insetPadding) 97 | ``` 98 | 99 | Set editor background color: 100 | 101 | ```swift 102 | TextEditorPlus(text: $text) 103 | .textSetting(NSColor.red, for: .backgroundColor) 104 | ``` 105 | 106 | Set editor text color: 107 | 108 | ```swift 109 | TextEditorPlus(text: $text) 110 | .textSetting(NSColor.red, for: .textColor) 111 | ``` 112 | 113 | Set editor placeholder string: 114 | 115 | ```swift 116 | TextEditorPlus(text: $text) 117 | //.font(NSFont(name: "pencontrol", size: 12)!) 118 | .font(.systemFont(ofSize: CGFloat(Float(fontSize)!), weight: .regular)) 119 | .textSetting("Test placeholder string", for: .placeholderString) 120 | ``` 121 | 122 | Manipulate attributed strings with attributes such as visual styles, hyperlinks, or accessibility data for portions of the text. 123 | 124 | ```swift 125 | TextEditorPlus(text: $text) 126 | .textSetting(isEditable, for: .isEditable) 127 | .textViewAttributedString(action: { val in 128 | let style = NSMutableParagraphStyle() 129 | style.lineSpacing = 5 130 | style.lineHeightMultiple = 1.2 131 | val.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: val.length)) 132 | return val 133 | }) 134 | ```` 135 | 136 | ## NSMutableAttributedString Support 137 | 138 | You can now use `TextEditorPlus` with `NSMutableAttributedString` for more advanced text formatting: 139 | 140 | ```swift 141 | import TextEditorPlus 142 | 143 | struct ContentView: View { 144 | @State var attributedText = NSMutableAttributedString(string: """ 145 | This is an example of NSMutableAttributedString. 146 | You can apply rich text formatting directly! 147 | """) 148 | 149 | var body: some View { 150 | VStack { 151 | // Using NSMutableAttributedString binding 152 | TextEditorPlus(text: $attributedText) 153 | .textSetting(true, for: .isEditable) 154 | .onAppear { 155 | setupAttributedText() 156 | } 157 | 158 | Button("Add Formatting") { 159 | applyFormatting() 160 | } 161 | } 162 | } 163 | 164 | func setupAttributedText() { 165 | let fullRange = NSRange(location: 0, length: attributedText.length) 166 | 167 | // Set base font 168 | #if os(iOS) 169 | attributedText.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: fullRange) 170 | #else 171 | attributedText.addAttribute(.font, value: NSFont.systemFont(ofSize: 16), range: fullRange) 172 | #endif 173 | 174 | // Apply paragraph style 175 | let paragraphStyle = NSMutableParagraphStyle() 176 | paragraphStyle.lineSpacing = 3 177 | paragraphStyle.paragraphSpacing = 8 178 | attributedText.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange) 179 | } 180 | 181 | func applyFormatting() { 182 | let highlightText = "NSMutableAttributedString" 183 | let range = (attributedText.string as NSString).range(of: highlightText) 184 | if range.location != NSNotFound { 185 | #if os(iOS) 186 | attributedText.addAttribute(.backgroundColor, value: UIColor.systemBlue, range: range) 187 | attributedText.addAttribute(.foregroundColor, value: UIColor.white, range: range) 188 | #else 189 | attributedText.addAttribute(.backgroundColor, value: NSColor.systemBlue, range: range) 190 | attributedText.addAttribute(.foregroundColor, value: NSColor.white, range: range) 191 | #endif 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | The `TextEditorPlus` now supports both `String` and `NSMutableAttributedString` bindings: 198 | 199 | - `TextEditorPlus(text: $text)` - for `String` binding 200 | - `TextEditorPlus(text: $attributedText)` - for `NSMutableAttributedString` binding 201 | 202 | When using `NSMutableAttributedString`, the text view will preserve all formatting attributes and allow direct manipulation of the attributed string. 203 | 204 | ## License 205 | 206 | Licensed under the MIT License. 207 | -------------------------------------------------------------------------------- /Sources/TextEditorPlus/TextEditorPlus.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | import SwiftUI 5 | 6 | #if os(OSX) 7 | import AppKit 8 | private typealias ViewRepresentable = NSViewRepresentable 9 | #elseif os(iOS) 10 | import UIKit 11 | private typealias ViewRepresentable = UIViewRepresentable 12 | #endif 13 | 14 | 15 | /// An enhanced version similar to `TextEditor`, aimed at maintaining consistency in its usage across iOS and macOS platforms. 16 | /// 17 | /// ```swift 18 | /// import TextEditorPlus 19 | /// 20 | /// struct ContentView: View { 21 | /// @State var text = """ 22 | /// Hello World 23 | /// """ 24 | /// @State var isEditable = true 25 | /// var body: some View { 26 | /// TextEditorPlus(text: $text) 27 | /// .textSetting(isEditable, for: .isEditable) 28 | /// } 29 | /// } 30 | /// ``` 31 | /// 32 | /// Set text weight and size: 33 | /// 34 | /// ```swift 35 | /// TextEditorPlus(text: $text) 36 | /// .font(.systemFont(ofSize: 24, weight: .regular)) 37 | /// ``` 38 | /// 39 | /// Set editor padding: 40 | /// 41 | /// ```swift 42 | /// TextEditorPlus(text: $text) 43 | /// .textSetting(23, for: .insetPadding) 44 | /// ``` 45 | /// 46 | /// Set editor background color: 47 | /// 48 | /// ```swift 49 | /// TextEditorPlus(text: $text) 50 | /// .textSetting(NSColor.red, for: .backgroundColor) 51 | /// ``` 52 | /// 53 | /// Manipulate attributed strings with attributes such as visual styles, hyperlinks, or accessibility data for portions of the text. 54 | /// 55 | /// ```swift 56 | /// TextEditorPlus(text: $text) 57 | /// .textSetting(isEditable, for: .isEditable) 58 | /// .textViewAttributedString(action: { val in 59 | /// let style = NSMutableParagraphStyle() 60 | /// style.lineSpacing = 5 61 | /// style.lineHeightMultiple = 1.2 62 | /// val.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: val.length)) 63 | /// return val 64 | /// }) 65 | /// ```` 66 | @available(iOS 13.0, macOS 10.15, *) 67 | public struct TextEditorPlus: ViewRepresentable { 68 | @Binding var text: String 69 | @Binding var attributedText: NSMutableAttributedString? 70 | internal var isAttributedTextMode: Bool 71 | @Environment(\.textViewIsEditable) private var isEditable 72 | @Environment(\.textViewInsetPadding) private var insetPadding 73 | @Environment(\.textViewAttributedString) private var textViewAttributedString 74 | @Environment(\.textViewBackgroundColor) private var textViewBackgroundColor 75 | @Environment(\.textViewPlaceholderString) private var placeholderString 76 | @Environment(\.colorScheme) var colorScheme 77 | @Font var font: FontHelper = .systemFont(ofSize: 14, weight: .regular) 78 | 79 | public init(text: Binding) { 80 | self._text = text 81 | self._attributedText = .constant(nil) 82 | self.isAttributedTextMode = false 83 | } 84 | 85 | public init(text: Binding) { 86 | self._text = .constant("") 87 | self._attributedText = Binding( 88 | get: { text.wrappedValue }, 89 | set: { newValue in 90 | if let newValue = newValue { 91 | text.wrappedValue = newValue 92 | } 93 | } 94 | ) 95 | self.isAttributedTextMode = true 96 | } 97 | 98 | private var currentText: String { 99 | if isAttributedTextMode { 100 | return attributedText?.string ?? "" 101 | } else { 102 | return text 103 | } 104 | } 105 | 106 | private var currentAttributedText: NSMutableAttributedString? { 107 | if isAttributedTextMode { 108 | return attributedText 109 | } else { 110 | return nil 111 | } 112 | } 113 | 114 | #if os(iOS) 115 | public func makeCoordinator() -> Coordinator { 116 | Coordinator(self) 117 | } 118 | public func makeUIView(context: Context) -> TextViewPlus { 119 | let textView = TextViewPlus() 120 | textView.isScrollEnabled = true 121 | textView.isEditable = true 122 | textView.font = UIFont.preferredFont(forTextStyle: .body) 123 | textView.delegate = context.coordinator 124 | 125 | // 设置文本内容 126 | if isAttributedTextMode { 127 | if let attributedText = attributedText { 128 | textView.attributedText = attributedText 129 | } 130 | } else { 131 | textView.text = text 132 | } 133 | 134 | textView.font = font 135 | textView.backgroundColor = textViewBackgroundColor ?? UIColor.clear 136 | textView.placeholderString = placeholderString ?? "" 137 | textView.placeholderFont = font 138 | // 关闭自动拼写、自动更正等特性 139 | textView.autocorrectionType = .no 140 | textView.spellCheckingType = .no 141 | textView.smartDashesType = .no 142 | textView.smartQuotesType = .no 143 | textView.smartInsertDeleteType = .no 144 | // 解决边距问题 145 | textView.placeholderInsetPadding = insetPadding 146 | textView.textContainerInset = UIEdgeInsets(top: insetPadding, left: insetPadding, bottom: insetPadding, right: insetPadding) 147 | return textView 148 | } 149 | 150 | public func updateUIView(_ uiView: TextViewPlus, context: Context) { 151 | // 根据模式更新文本内容 152 | if isAttributedTextMode { 153 | if let attributedText = attributedText, 154 | uiView.attributedText.string != attributedText.string { 155 | uiView.attributedText = attributedText 156 | } 157 | } else { 158 | // 只在内容变化时赋值,避免大文本频繁刷新 159 | if uiView.text != text { 160 | uiView.text = text 161 | } 162 | } 163 | 164 | uiView.isEditable = isEditable 165 | if uiView.font != font { 166 | uiView.font = font 167 | } 168 | uiView.placeholderFont = font 169 | // 解决边距问题 170 | uiView.placeholderInsetPadding = insetPadding 171 | uiView.textContainerInset = UIEdgeInsets(top: insetPadding, left: insetPadding, bottom: insetPadding, right: insetPadding) 172 | uiView.backgroundColor = textViewBackgroundColor ?? UIColor.clear 173 | uiView.placeholderString = placeholderString ?? "" 174 | 175 | // 强制重新绘制以确保边距一致性 176 | uiView.setNeedsDisplay() 177 | 178 | // 强制布局更新以确保textContainerInset立即生效 179 | uiView.setNeedsLayout() 180 | uiView.layoutIfNeeded() 181 | 182 | // 只有在非属性字符串模式下才应用 textViewAttributedString 处理 183 | if !isAttributedTextMode && !text.isEmpty { 184 | let attributedString = NSMutableAttributedString(string: text) 185 | let nsColor = colorScheme == .dark ? UIColor.white : UIColor.black 186 | attributedString.addAttribute(.foregroundColor, value: nsColor, range: NSRange(location: 0, length: attributedString.length)) 187 | attributedString.addAttribute(.font, value: font as Any, range: NSRange(location: 0, length: attributedString.length)) 188 | if textViewAttributedString(attributedString) != nil { 189 | uiView.textStorage.setAttributedString(attributedString) 190 | } 191 | } 192 | } 193 | #elseif os(OSX) 194 | public func makeCoordinator() -> Coordinator { 195 | Coordinator(self) 196 | } 197 | 198 | public func makeNSView(context: Context) -> NSScrollView { 199 | let scrollView = NSScrollView() 200 | scrollView.drawsBackground = true 201 | scrollView.borderType = .noBorder 202 | scrollView.hasVerticalScroller = true 203 | scrollView.hasHorizontalRuler = false 204 | scrollView.autoresizingMask = [.width, .height] 205 | scrollView.autohidesScrollers = true 206 | scrollView.translatesAutoresizingMaskIntoConstraints = false 207 | let contentSize = scrollView.contentSize 208 | let textStorage = NSTextStorage() 209 | 210 | 211 | let layoutManager = NSLayoutManager() 212 | textStorage.addLayoutManager(layoutManager) 213 | 214 | let textContainer = NSTextContainer(containerSize: scrollView.frame.size) 215 | textContainer.widthTracksTextView = true 216 | textContainer.containerSize = NSSize( 217 | width: contentSize.width, 218 | height: CGFloat.greatestFiniteMagnitude 219 | ) 220 | layoutManager.addTextContainer(textContainer) 221 | 222 | 223 | let textView = TextViewPlus(frame: .zero, textContainer: textContainer) 224 | textView.autoresizingMask = .width 225 | if let bgColor = textViewBackgroundColor { 226 | textView.backgroundColor = bgColor 227 | textView.drawsBackground = true 228 | } else { 229 | textView.backgroundColor = NSColor.clear 230 | textView.drawsBackground = false 231 | } 232 | textView.isEditable = isEditable 233 | textView.isHorizontallyResizable = false 234 | textView.isRichText = false 235 | textView.isVerticallyResizable = true 236 | textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 237 | textView.minSize = NSSize(width: 0, height: contentSize.height) 238 | textView.allowsUndo = true 239 | textView.delegate = context.coordinator // 设置代理 240 | textView.font = font 241 | textView.placeholderString = placeholderString ?? "" 242 | 243 | // 设置文本内容 244 | if isAttributedTextMode { 245 | if let attributedText = attributedText { 246 | textView.textStorage?.setAttributedString(attributedText) 247 | } 248 | } else { 249 | textView.string = text 250 | } 251 | 252 | // 关闭自动拼写检查等特性 253 | textView.isContinuousSpellCheckingEnabled = false 254 | textView.isAutomaticSpellingCorrectionEnabled = false 255 | textView.isGrammarCheckingEnabled = false 256 | textView.isAutomaticQuoteSubstitutionEnabled = false 257 | textView.isAutomaticDashSubstitutionEnabled = false 258 | textView.isAutomaticTextReplacementEnabled = false 259 | textView.isAutomaticLinkDetectionEnabled = false 260 | textView.isAutomaticDataDetectionEnabled = false 261 | textView.isAutomaticTextCompletionEnabled = false 262 | 263 | textView.registerForDraggedTypes([.string]) 264 | // 解决边距问题 265 | textView.placeholderInsetPadding = insetPadding 266 | textView.textContainerInset = NSSize(width: insetPadding, height: insetPadding) 267 | textView.textContainer?.lineFragmentPadding = 0 // 避免双重边距 268 | 269 | scrollView.translatesAutoresizingMaskIntoConstraints = false 270 | 271 | scrollView.documentView = textView 272 | return scrollView 273 | } 274 | 275 | public func updateNSView(_ scrollView: NSScrollView, context: Context) { 276 | if let textView = scrollView.documentView as? TextViewPlus { 277 | // 根据模式更新文本内容 278 | if isAttributedTextMode { 279 | if let attributedText = attributedText, 280 | textView.textStorage?.string != attributedText.string { 281 | textView.textStorage?.setAttributedString(attributedText) 282 | } 283 | } else { 284 | // 只在内容变化时赋值,避免大文本频繁刷新 285 | if textView.string != text { 286 | textView.string = text 287 | } 288 | } 289 | 290 | if let bgColor = textViewBackgroundColor { 291 | textView.backgroundColor = bgColor 292 | textView.drawsBackground = true 293 | } else { 294 | textView.backgroundColor = NSColor.clear 295 | textView.drawsBackground = false 296 | } 297 | textView.isEditable = isEditable 298 | if textView.font != font { 299 | textView.font = font 300 | } 301 | textView.placeholderString = placeholderString ?? "" 302 | textView.placeholderInsetPadding = insetPadding 303 | textView.textContainerInset = NSSize(width: insetPadding, height: insetPadding) 304 | textView.textContainer?.lineFragmentPadding = 0 // 避免双重边距 305 | 306 | // 强制重新绘制以确保边距一致性 307 | textView.needsDisplay = true 308 | 309 | // 只有在非属性字符串模式下才应用 textViewAttributedString 处理 310 | if !isAttributedTextMode && !text.isEmpty { 311 | let attributedString = NSMutableAttributedString(string: text) 312 | let nsColor = colorScheme == .dark ? NSColor.white : NSColor.black 313 | attributedString.addAttribute(.foregroundColor, value: nsColor, range: NSRange(location: 0, length: attributedString.length)) 314 | attributedString.addAttribute(.font, value: font as Any, range: NSRange(location: 0, length: attributedString.length)) 315 | if textViewAttributedString(attributedString) != nil { 316 | textView.textStorage?.setAttributedString(attributedString) 317 | } 318 | } 319 | 320 | if context.coordinator.selectedRanges.count > 0 { 321 | textView.selectedRanges = context.coordinator.selectedRanges 322 | } 323 | } 324 | } 325 | #endif 326 | } 327 | 328 | #if os(OSX) 329 | @available(macOS 10.15, *) 330 | class TextViewPlus: NSTextView { 331 | var placeholderString: String = "" { 332 | didSet { 333 | self.needsDisplay = true 334 | } 335 | } 336 | var placeholderInsetPadding: CGFloat = 18 { 337 | didSet { 338 | self.needsDisplay = true 339 | } 340 | } 341 | override func draw(_ dirtyRect: NSRect) { 342 | super.draw(dirtyRect) 343 | 344 | let shouldShowPlaceholder = string.isEmpty && textStorage?.length == 0 345 | if shouldShowPlaceholder && !placeholderString.isEmpty { 346 | let attributes: [NSAttributedString.Key: Any] = [ 347 | .foregroundColor: NSColor.placeholderTextColor, 348 | .font: self.font as Any 349 | ] 350 | let padding = placeholderInsetPadding 351 | // 与 textContainerInset 保持一致的边距 352 | let rect = CGRect(x: padding, y: padding, width: self.bounds.width - padding * 2, height: self.bounds.height - padding * 2) 353 | placeholderString.draw(in: rect, withAttributes: attributes) 354 | } 355 | } 356 | } 357 | #endif 358 | 359 | #if os(iOS) 360 | @available(iOS 13.0, *) 361 | 362 | public class TextViewPlus: UITextView { 363 | var placeholderString: String = "" { 364 | didSet { 365 | setNeedsDisplay() 366 | } 367 | } 368 | var placeholderFont: FontHelper? { 369 | didSet { 370 | setNeedsDisplay() 371 | } 372 | } 373 | var placeholderInsetPadding: CGFloat = 18 { 374 | didSet { 375 | setNeedsDisplay() 376 | } 377 | } 378 | public override func draw(_ rect: CGRect) { 379 | super.draw(rect) 380 | 381 | let shouldShowPlaceholder = text.isEmpty && attributedText.length == 0 382 | if shouldShowPlaceholder && !placeholderString.isEmpty { 383 | let font = placeholderFont != nil ? placeholderFont : self.font 384 | let attributes: [NSAttributedString.Key: Any] = [ 385 | .foregroundColor: UIColor.placeholderText, 386 | .font: font as Any 387 | ] 388 | let padding = placeholderInsetPadding 389 | // 与 textContainerInset 保持一致的边距 390 | let rect = CGRect(x: padding, y: padding, width: self.bounds.width - padding * 2, height: self.bounds.height - padding * 2) 391 | placeholderString.draw(in: rect, withAttributes: attributes) 392 | } 393 | } 394 | } 395 | #endif 396 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7D21058C2C0EAC5D004E0E77 /* TextColorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D21058B2C0EAC5D004E0E77 /* TextColorView.swift */; }; 11 | 7D4E4F192BB2E5AF003B3098 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4E4F182BB2E5AF003B3098 /* PlaceholderView.swift */; }; 12 | 7D4E4F5F2BB3E4A7003B3098 /* OnChangeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4E4F5E2BB3E4A7003B3098 /* OnChangeView.swift */; }; 13 | 7DAB5BE42BB0ABA200B5146A /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAB5BE32BB0ABA200B5146A /* ExampleApp.swift */; }; 14 | 7DAB5BE62BB0ABA200B5146A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAB5BE52BB0ABA200B5146A /* ContentView.swift */; }; 15 | 7DAB5BE82BB0ABA500B5146A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7DAB5BE72BB0ABA500B5146A /* Assets.xcassets */; }; 16 | 7DAB5BEC2BB0ABA500B5146A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7DAB5BEB2BB0ABA500B5146A /* Preview Assets.xcassets */; }; 17 | 7DAB5BF52BB0ACAE00B5146A /* TextEditorPlus in Frameworks */ = {isa = PBXBuildFile; productRef = 7DAB5BF42BB0ACAE00B5146A /* TextEditorPlus */; }; 18 | 7DAB5BF82BB0AF9000B5146A /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAB5BF72BB0AF8F00B5146A /* ExampleView.swift */; }; 19 | 7DAB5BFA2BB16B0D00B5146A /* FontExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAB5BF92BB16B0D00B5146A /* FontExampleView.swift */; }; 20 | 7DC4710A2BB1FCAF00CE7718 /* MutableAttributedStringExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC471092BB1FCAF00CE7718 /* MutableAttributedStringExampleView.swift */; }; 21 | E045F2672E51B8BE009F7AE0 /* Sdifft in Frameworks */ = {isa = PBXBuildFile; productRef = E045F2662E51B8BE009F7AE0 /* Sdifft */; }; 22 | E045F2692E51B8DC009F7AE0 /* TextDiffView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E045F2682E51B8D5009F7AE0 /* TextDiffView.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 7D21058B2C0EAC5D004E0E77 /* TextColorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextColorView.swift; sourceTree = ""; }; 27 | 7D4E4F182BB2E5AF003B3098 /* PlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderView.swift; sourceTree = ""; }; 28 | 7D4E4F5E2BB3E4A7003B3098 /* OnChangeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnChangeView.swift; sourceTree = ""; }; 29 | 7DAB5BE02BB0ABA200B5146A /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 7DAB5BE32BB0ABA200B5146A /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 31 | 7DAB5BE52BB0ABA200B5146A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 32 | 7DAB5BE72BB0ABA500B5146A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 7DAB5BE92BB0ABA500B5146A /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 34 | 7DAB5BEB2BB0ABA500B5146A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 35 | 7DAB5BF32BB0ACA400B5146A /* swiftui-texteditor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "swiftui-texteditor"; path = ..; sourceTree = ""; }; 36 | 7DAB5BF72BB0AF8F00B5146A /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; 37 | 7DAB5BF92BB16B0D00B5146A /* FontExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExampleView.swift; sourceTree = ""; }; 38 | 7DC471092BB1FCAF00CE7718 /* MutableAttributedStringExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableAttributedStringExampleView.swift; sourceTree = ""; }; 39 | E045F2682E51B8D5009F7AE0 /* TextDiffView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDiffView.swift; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | 7DAB5BDD2BB0ABA200B5146A /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | E045F2672E51B8BE009F7AE0 /* Sdifft in Frameworks */, 48 | 7DAB5BF52BB0ACAE00B5146A /* TextEditorPlus in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 7DAB5BD72BB0ABA200B5146A = { 56 | isa = PBXGroup; 57 | children = ( 58 | 7DAB5BE22BB0ABA200B5146A /* Example */, 59 | 7DAB5BE12BB0ABA200B5146A /* Products */, 60 | 7DAB5BF22BB0ACA400B5146A /* Frameworks */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 7DAB5BE12BB0ABA200B5146A /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 7DAB5BE02BB0ABA200B5146A /* Example.app */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | 7DAB5BE22BB0ABA200B5146A /* Example */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 7DAB5BF62BB0AF5000B5146A /* Examples */, 76 | 7DAB5BE32BB0ABA200B5146A /* ExampleApp.swift */, 77 | 7DAB5BE52BB0ABA200B5146A /* ContentView.swift */, 78 | 7DAB5BE72BB0ABA500B5146A /* Assets.xcassets */, 79 | 7DAB5BE92BB0ABA500B5146A /* Example.entitlements */, 80 | 7DAB5BEA2BB0ABA500B5146A /* Preview Content */, 81 | ); 82 | path = Example; 83 | sourceTree = ""; 84 | }; 85 | 7DAB5BEA2BB0ABA500B5146A /* Preview Content */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 7DAB5BEB2BB0ABA500B5146A /* Preview Assets.xcassets */, 89 | ); 90 | path = "Preview Content"; 91 | sourceTree = ""; 92 | }; 93 | 7DAB5BF22BB0ACA400B5146A /* Frameworks */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 7DAB5BF32BB0ACA400B5146A /* swiftui-texteditor */, 97 | ); 98 | name = Frameworks; 99 | sourceTree = ""; 100 | }; 101 | 7DAB5BF62BB0AF5000B5146A /* Examples */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | E045F2682E51B8D5009F7AE0 /* TextDiffView.swift */, 105 | 7DAB5BF72BB0AF8F00B5146A /* ExampleView.swift */, 106 | 7DAB5BF92BB16B0D00B5146A /* FontExampleView.swift */, 107 | 7DC471092BB1FCAF00CE7718 /* MutableAttributedStringExampleView.swift */, 108 | 7D4E4F182BB2E5AF003B3098 /* PlaceholderView.swift */, 109 | 7D4E4F5E2BB3E4A7003B3098 /* OnChangeView.swift */, 110 | 7D21058B2C0EAC5D004E0E77 /* TextColorView.swift */, 111 | ); 112 | path = Examples; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 7DAB5BDF2BB0ABA200B5146A /* Example */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 7DAB5BEF2BB0ABA500B5146A /* Build configuration list for PBXNativeTarget "Example" */; 121 | buildPhases = ( 122 | 7DAB5BDC2BB0ABA200B5146A /* Sources */, 123 | 7DAB5BDD2BB0ABA200B5146A /* Frameworks */, 124 | 7DAB5BDE2BB0ABA200B5146A /* Resources */, 125 | ); 126 | buildRules = ( 127 | ); 128 | dependencies = ( 129 | ); 130 | name = Example; 131 | packageProductDependencies = ( 132 | 7DAB5BF42BB0ACAE00B5146A /* TextEditorPlus */, 133 | E045F2662E51B8BE009F7AE0 /* Sdifft */, 134 | ); 135 | productName = Example; 136 | productReference = 7DAB5BE02BB0ABA200B5146A /* Example.app */; 137 | productType = "com.apple.product-type.application"; 138 | }; 139 | /* End PBXNativeTarget section */ 140 | 141 | /* Begin PBXProject section */ 142 | 7DAB5BD82BB0ABA200B5146A /* Project object */ = { 143 | isa = PBXProject; 144 | attributes = { 145 | BuildIndependentTargetsInParallel = 1; 146 | LastSwiftUpdateCheck = 1530; 147 | LastUpgradeCheck = 1530; 148 | TargetAttributes = { 149 | 7DAB5BDF2BB0ABA200B5146A = { 150 | CreatedOnToolsVersion = 15.3; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = 7DAB5BDB2BB0ABA200B5146A /* Build configuration list for PBXProject "Example" */; 155 | compatibilityVersion = "Xcode 14.0"; 156 | developmentRegion = en; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | en, 160 | Base, 161 | ); 162 | mainGroup = 7DAB5BD72BB0ABA200B5146A; 163 | packageReferences = ( 164 | E045F2652E51B8BE009F7AE0 /* XCRemoteSwiftPackageReference "Sdifft" */, 165 | ); 166 | productRefGroup = 7DAB5BE12BB0ABA200B5146A /* Products */; 167 | projectDirPath = ""; 168 | projectRoot = ""; 169 | targets = ( 170 | 7DAB5BDF2BB0ABA200B5146A /* Example */, 171 | ); 172 | }; 173 | /* End PBXProject section */ 174 | 175 | /* Begin PBXResourcesBuildPhase section */ 176 | 7DAB5BDE2BB0ABA200B5146A /* Resources */ = { 177 | isa = PBXResourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 7DAB5BEC2BB0ABA500B5146A /* Preview Assets.xcassets in Resources */, 181 | 7DAB5BE82BB0ABA500B5146A /* Assets.xcassets in Resources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXResourcesBuildPhase section */ 186 | 187 | /* Begin PBXSourcesBuildPhase section */ 188 | 7DAB5BDC2BB0ABA200B5146A /* Sources */ = { 189 | isa = PBXSourcesBuildPhase; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | 7DAB5BE62BB0ABA200B5146A /* ContentView.swift in Sources */, 193 | 7DAB5BE42BB0ABA200B5146A /* ExampleApp.swift in Sources */, 194 | 7D4E4F192BB2E5AF003B3098 /* PlaceholderView.swift in Sources */, 195 | 7DAB5BFA2BB16B0D00B5146A /* FontExampleView.swift in Sources */, 196 | E045F2692E51B8DC009F7AE0 /* TextDiffView.swift in Sources */, 197 | 7D4E4F5F2BB3E4A7003B3098 /* OnChangeView.swift in Sources */, 198 | 7D21058C2C0EAC5D004E0E77 /* TextColorView.swift in Sources */, 199 | 7DC4710A2BB1FCAF00CE7718 /* MutableAttributedStringExampleView.swift in Sources */, 200 | 7DAB5BF82BB0AF9000B5146A /* ExampleView.swift in Sources */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | /* End PBXSourcesBuildPhase section */ 205 | 206 | /* Begin XCBuildConfiguration section */ 207 | 7DAB5BED2BB0ABA500B5146A /* Debug */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ALWAYS_SEARCH_USER_PATHS = NO; 211 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 212 | CLANG_ANALYZER_NONNULL = YES; 213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 215 | CLANG_ENABLE_MODULES = YES; 216 | CLANG_ENABLE_OBJC_ARC = YES; 217 | CLANG_ENABLE_OBJC_WEAK = YES; 218 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 219 | CLANG_WARN_BOOL_CONVERSION = YES; 220 | CLANG_WARN_COMMA = YES; 221 | CLANG_WARN_CONSTANT_CONVERSION = YES; 222 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 225 | CLANG_WARN_EMPTY_BODY = YES; 226 | CLANG_WARN_ENUM_CONVERSION = YES; 227 | CLANG_WARN_INFINITE_RECURSION = YES; 228 | CLANG_WARN_INT_CONVERSION = YES; 229 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 230 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 231 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 233 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 235 | CLANG_WARN_STRICT_PROTOTYPES = YES; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | COPY_PHASE_STRIP = NO; 241 | DEBUG_INFORMATION_FORMAT = dwarf; 242 | ENABLE_STRICT_OBJC_MSGSEND = YES; 243 | ENABLE_TESTABILITY = YES; 244 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu17; 246 | GCC_DYNAMIC_NO_PIC = NO; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 260 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 261 | MTL_FAST_MATH = YES; 262 | ONLY_ACTIVE_ARCH = YES; 263 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 264 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 265 | }; 266 | name = Debug; 267 | }; 268 | 7DAB5BEE2BB0ABA500B5146A /* Release */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ALWAYS_SEARCH_USER_PATHS = NO; 272 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 276 | CLANG_ENABLE_MODULES = YES; 277 | CLANG_ENABLE_OBJC_ARC = YES; 278 | CLANG_ENABLE_OBJC_WEAK = YES; 279 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 280 | CLANG_WARN_BOOL_CONVERSION = YES; 281 | CLANG_WARN_COMMA = YES; 282 | CLANG_WARN_CONSTANT_CONVERSION = YES; 283 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 284 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 285 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 286 | CLANG_WARN_EMPTY_BODY = YES; 287 | CLANG_WARN_ENUM_CONVERSION = YES; 288 | CLANG_WARN_INFINITE_RECURSION = YES; 289 | CLANG_WARN_INT_CONVERSION = YES; 290 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 292 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 294 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | COPY_PHASE_STRIP = NO; 302 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 303 | ENABLE_NS_ASSERTIONS = NO; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 306 | GCC_C_LANGUAGE_STANDARD = gnu17; 307 | GCC_NO_COMMON_BLOCKS = YES; 308 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 309 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 310 | GCC_WARN_UNDECLARED_SELECTOR = YES; 311 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 312 | GCC_WARN_UNUSED_FUNCTION = YES; 313 | GCC_WARN_UNUSED_VARIABLE = YES; 314 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 315 | MTL_ENABLE_DEBUG_INFO = NO; 316 | MTL_FAST_MATH = YES; 317 | SWIFT_COMPILATION_MODE = wholemodule; 318 | }; 319 | name = Release; 320 | }; 321 | 7DAB5BF02BB0ABA500B5146A /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 325 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 326 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 327 | CODE_SIGN_STYLE = Automatic; 328 | CURRENT_PROJECT_VERSION = 1; 329 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 330 | DEVELOPMENT_TEAM = GR99S2ZJZQ; 331 | ENABLE_HARDENED_RUNTIME = YES; 332 | ENABLE_PREVIEWS = YES; 333 | GENERATE_INFOPLIST_FILE = YES; 334 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 335 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 336 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 337 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 338 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 339 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 340 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 341 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 342 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 343 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 344 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 345 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 346 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 347 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 348 | MACOSX_DEPLOYMENT_TARGET = 14.0; 349 | MARKETING_VERSION = 1.0; 350 | PRODUCT_BUNDLE_IDENTIFIER = com.wangchujiang.Example; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | SDKROOT = auto; 353 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 354 | SWIFT_EMIT_LOC_STRINGS = YES; 355 | SWIFT_VERSION = 5.0; 356 | TARGETED_DEVICE_FAMILY = "1,2"; 357 | }; 358 | name = Debug; 359 | }; 360 | 7DAB5BF12BB0ABA500B5146A /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 365 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 366 | CODE_SIGN_STYLE = Automatic; 367 | CURRENT_PROJECT_VERSION = 1; 368 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 369 | DEVELOPMENT_TEAM = GR99S2ZJZQ; 370 | ENABLE_HARDENED_RUNTIME = YES; 371 | ENABLE_PREVIEWS = YES; 372 | GENERATE_INFOPLIST_FILE = YES; 373 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 374 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 375 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 376 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 377 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 378 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 379 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 380 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 381 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 382 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 383 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 384 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 385 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 386 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 387 | MACOSX_DEPLOYMENT_TARGET = 14.0; 388 | MARKETING_VERSION = 1.0; 389 | PRODUCT_BUNDLE_IDENTIFIER = com.wangchujiang.Example; 390 | PRODUCT_NAME = "$(TARGET_NAME)"; 391 | SDKROOT = auto; 392 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 393 | SWIFT_EMIT_LOC_STRINGS = YES; 394 | SWIFT_VERSION = 5.0; 395 | TARGETED_DEVICE_FAMILY = "1,2"; 396 | }; 397 | name = Release; 398 | }; 399 | /* End XCBuildConfiguration section */ 400 | 401 | /* Begin XCConfigurationList section */ 402 | 7DAB5BDB2BB0ABA200B5146A /* Build configuration list for PBXProject "Example" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | 7DAB5BED2BB0ABA500B5146A /* Debug */, 406 | 7DAB5BEE2BB0ABA500B5146A /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Release; 410 | }; 411 | 7DAB5BEF2BB0ABA500B5146A /* Build configuration list for PBXNativeTarget "Example" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | 7DAB5BF02BB0ABA500B5146A /* Debug */, 415 | 7DAB5BF12BB0ABA500B5146A /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Release; 419 | }; 420 | /* End XCConfigurationList section */ 421 | 422 | /* Begin XCRemoteSwiftPackageReference section */ 423 | E045F2652E51B8BE009F7AE0 /* XCRemoteSwiftPackageReference "Sdifft" */ = { 424 | isa = XCRemoteSwiftPackageReference; 425 | repositoryURL = "https://github.com/wzxha/Sdifft.git"; 426 | requirement = { 427 | kind = exactVersion; 428 | version = 2.1.0; 429 | }; 430 | }; 431 | /* End XCRemoteSwiftPackageReference section */ 432 | 433 | /* Begin XCSwiftPackageProductDependency section */ 434 | 7DAB5BF42BB0ACAE00B5146A /* TextEditorPlus */ = { 435 | isa = XCSwiftPackageProductDependency; 436 | productName = TextEditorPlus; 437 | }; 438 | E045F2662E51B8BE009F7AE0 /* Sdifft */ = { 439 | isa = XCSwiftPackageProductDependency; 440 | package = E045F2652E51B8BE009F7AE0 /* XCRemoteSwiftPackageReference "Sdifft" */; 441 | productName = Sdifft; 442 | }; 443 | /* End XCSwiftPackageProductDependency section */ 444 | }; 445 | rootObject = 7DAB5BD82BB0ABA200B5146A /* Project object */; 446 | } 447 | --------------------------------------------------------------------------------