├── 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 |

5 |

6 |

7 |

8 |

9 |

10 |

11 |

12 |

13 |

14 |

15 |

16 |

17 |

18 |

19 |

20 |

21 |

22 |

23 |

24 |

25 |

26 |

27 |

28 |

29 |

30 |

31 |

32 |

33 |

34 |
35 |
36 |
37 | SwiftUI TextEditorPlus
38 | ===
39 |
40 | [](https://jaywcjlove.github.io/#/sponsor)
41 | [](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 |
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 |
--------------------------------------------------------------------------------