├── Demon
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
└── ViewController.swift
├── IMITextView.podspec
├── IMITextView.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── Demon.xcscheme
├── IMITextView
├── Classes
│ ├── IMITextView.swift
│ ├── IMTextLayoutManager.swift
│ ├── IMTextViewConfiguration.swift
│ └── IMUITextView.swift
├── IMITextView.h
└── Info.plist
├── Images
├── demon001.png
├── demon002.png
└── demon003.png
├── LICENSE
├── Package.swift
└── README.md
/Demon/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demon
4 | //
5 | // Created by immortal on 2021/4/29
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/Demon/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 |
--------------------------------------------------------------------------------
/Demon/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demon/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demon/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Demon/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Demon/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSupportsIndirectInputEvents
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Demon/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Demon
4 | //
5 | // Created by immortal on 2021/4/29
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import IMITextView
11 |
12 | class ViewController: UIViewController {
13 |
14 | private lazy var toolbar: UIToolbar = {
15 | let toolbar = UIToolbar()
16 | toolbar.tintColor = .white
17 | toolbar.barTintColor = .clear
18 | toolbar.backgroundColor = .clear
19 | toolbar.isTranslucent = false
20 | toolbar.translatesAutoresizingMaskIntoConstraints = false
21 | return toolbar
22 | }()
23 |
24 | private let alignBtn: UIButton = {
25 | let button = UIButton(type: .custom)
26 | button.translatesAutoresizingMaskIntoConstraints = false
27 | button.setTitle("Alignment: Center", for: .normal)
28 | button.setTitleColor(.white, for: .normal)
29 | button.titleLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .bold)
30 | return button
31 | }()
32 |
33 | private let lineBgdTypeBtn: UIButton = {
34 | let button = UIButton(type: .custom)
35 | button.translatesAutoresizingMaskIntoConstraints = false
36 | button.setTitle("Style: Fill", for: .normal)
37 | button.setTitleColor(.white, for: .normal)
38 | button.titleLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .bold)
39 | return button
40 | }()
41 |
42 | private let strokeBtn: UIButton = {
43 | let button = UIButton(type: .custom)
44 | button.translatesAutoresizingMaskIntoConstraints = false
45 | button.setTitle("Stroke", for: .normal)
46 | button.setTitleColor(.white, for: .normal)
47 | button.titleLabel?.font = UIFont.systemFont(ofSize: 15.0, weight: .bold)
48 | return button
49 | }()
50 |
51 | private let textView: IMITextView = {
52 | let textView = IMITextView()
53 | textView.translatesAutoresizingMaskIntoConstraints = false
54 | textView.configuration.lineBackgroundOptions = .fill
55 | textView.configuration.textAlignment = .center
56 | textView.configuration.strokeColor = .red
57 | textView.text = "Test Test Test Test Test\nTest Test\nTest Test Test Test"
58 | textView.tintColor = .red
59 | return textView
60 | }()
61 |
62 | override func viewDidLoad() {
63 | super.viewDidLoad()
64 | view.backgroundColor = .black
65 | loadSubviews()
66 |
67 | let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
68 | fixedSpace.width = 30
69 | toolbar.setItems([
70 | UIBarButtonItem(customView: alignBtn),
71 | fixedSpace,
72 | UIBarButtonItem(customView: lineBgdTypeBtn),
73 | fixedSpace,
74 | UIBarButtonItem(customView: strokeBtn)
75 | ], animated: false)
76 | alignBtn.addTarget(self, action: #selector(didChangeAlign(_:)), for: .touchUpInside)
77 | lineBgdTypeBtn.addTarget(self, action: #selector(didChangeLineBgd(_:)), for: .touchUpInside)
78 | strokeBtn.addTarget(self, action: #selector(didChangeStroke(_:)), for: .touchUpInside)
79 |
80 | textView.becomeFirstResponder()
81 | }
82 |
83 | private func loadSubviews() {
84 | view.addSubview(toolbar)
85 | NSLayoutConstraint.activate([
86 | toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
87 | toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
88 | toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor)
89 | ])
90 |
91 | view.addSubview(textView)
92 | NSLayoutConstraint.activate([
93 | textView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
94 | textView.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
95 | textView.widthAnchor.constraint(equalTo: view.widthAnchor),
96 | textView.heightAnchor.constraint(equalToConstant: 250.0)
97 | ])
98 |
99 | view.layoutIfNeeded()
100 | }
101 |
102 | @objc private func didChangeAlign(_ sender: UIButton) {
103 | textView.configuration.textAlignment = .init(rawValue: textView.configuration.textAlignment.rawValue + 1) ?? .left
104 | sender.setTitle("Alignment: \(String(describing: textView.configuration.textAlignment).capitalized)", for: .normal)
105 | }
106 |
107 | @objc private func didChangeLineBgd(_ sender: UIButton) {
108 | textView.configuration.lineBackgroundOptions = textView.configuration.lineBackgroundOptions == .fill ? .boder : .fill
109 | textView.configuration.textColor = textView.configuration.lineBackgroundOptions == .fill ? .black : .white
110 | sender.setTitle("Style: \(textView.configuration.lineBackgroundOptions == .boder ? "Boder" : "Fill")", for: .normal)
111 | }
112 |
113 | @objc private func didChangeStroke(_ sender: UIButton) {
114 | textView.configuration.strokeWidth = textView.configuration.strokeWidth == 0 ? 10.0 : 0.0
115 | }
116 |
117 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
118 | view.endEditing(true)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/IMITextView.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'IMITextView'
3 | s.version = '0.0.1'
4 | s.summary = 'Provide background effects of textView like Instagram in iOS'
5 |
6 | s.homepage = 'https://github.com/immortal-it/IMITextView'
7 | s.license = { :type => 'MIT', :file => 'LICENSE' }
8 | s.author = { 'Immortal' => 'immortal@gmail.com' }
9 | s.source = { :git => 'https://github.com/immortal-it/IMITextView.git', :tag => s.version }
10 |
11 | s.ios.deployment_target = '11.0'
12 | s.requires_arc = true
13 | s.swift_versions = ['5.1', '5.2', '5.3']
14 |
15 | s.source_files = 'IMITextView/**/*.{swift}'
16 |
17 | s.pod_target_xcconfig = {
18 | 'SWIFT_INSTALL_OBJC_HEADER' => 'NO'
19 | }
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/IMITextView.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 5C4C641F2646359C00FC1D5D /* IMTextLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C6417263BF77400FC1D5D /* IMTextLayoutManager.swift */; };
11 | 5C4C64202646359C00FC1D5D /* IMITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C641926439CF900FC1D5D /* IMITextView.swift */; };
12 | 5C4C642D26463C3C00FC1D5D /* IMUITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C642C26463C3C00FC1D5D /* IMUITextView.swift */; };
13 | 5C4C642F26463CD100FC1D5D /* IMTextViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C642E26463CD100FC1D5D /* IMTextViewConfiguration.swift */; };
14 | 5C92F0FC263A52030058FD6B /* IMITextView.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C92F0FA263A52030058FD6B /* IMITextView.h */; settings = {ATTRIBUTES = (Public, ); }; };
15 | 5C92F10A263A52270058FD6B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C92F109263A52270058FD6B /* AppDelegate.swift */; };
16 | 5C92F10E263A52270058FD6B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C92F10D263A52270058FD6B /* ViewController.swift */; };
17 | 5C92F111263A52270058FD6B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C92F10F263A52270058FD6B /* Main.storyboard */; };
18 | 5C92F113263A52280058FD6B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5C92F112263A52280058FD6B /* Assets.xcassets */; };
19 | 5C92F116263A52280058FD6B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5C92F114263A52280058FD6B /* LaunchScreen.storyboard */; };
20 | 5C92F124263A52F20058FD6B /* IMITextView.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5C92F0F7263A52030058FD6B /* IMITextView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXCopyFilesBuildPhase section */
24 | 5C92F123263A52DA0058FD6B /* CopyFiles */ = {
25 | isa = PBXCopyFilesBuildPhase;
26 | buildActionMask = 2147483647;
27 | dstPath = "";
28 | dstSubfolderSpec = 10;
29 | files = (
30 | 5C92F124263A52F20058FD6B /* IMITextView.framework in CopyFiles */,
31 | );
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXCopyFilesBuildPhase section */
35 |
36 | /* Begin PBXFileReference section */
37 | 5C4C6417263BF77400FC1D5D /* IMTextLayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMTextLayoutManager.swift; sourceTree = ""; };
38 | 5C4C641926439CF900FC1D5D /* IMITextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMITextView.swift; sourceTree = ""; };
39 | 5C4C642C26463C3C00FC1D5D /* IMUITextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMUITextView.swift; sourceTree = ""; };
40 | 5C4C642E26463CD100FC1D5D /* IMTextViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMTextViewConfiguration.swift; sourceTree = ""; };
41 | 5C4C643026464D1E00FC1D5D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
42 | 5C4C643126464D1E00FC1D5D /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
43 | 5C4C643526464D9D00FC1D5D /* IMITextView.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = IMITextView.podspec; sourceTree = ""; };
44 | 5C4C6436264650D300FC1D5D /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
45 | 5C92F0F7263A52030058FD6B /* IMITextView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IMITextView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
46 | 5C92F0FA263A52030058FD6B /* IMITextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IMITextView.h; sourceTree = ""; };
47 | 5C92F0FB263A52030058FD6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
48 | 5C92F107263A52270058FD6B /* Demon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demon.app; sourceTree = BUILT_PRODUCTS_DIR; };
49 | 5C92F109263A52270058FD6B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
50 | 5C92F10D263A52270058FD6B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
51 | 5C92F110263A52270058FD6B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
52 | 5C92F112263A52280058FD6B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
53 | 5C92F115263A52280058FD6B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
54 | 5C92F117263A52280058FD6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | 5C92F0F4263A52030058FD6B /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | );
63 | runOnlyForDeploymentPostprocessing = 0;
64 | };
65 | 5C92F104263A52270058FD6B /* Frameworks */ = {
66 | isa = PBXFrameworksBuildPhase;
67 | buildActionMask = 2147483647;
68 | files = (
69 | );
70 | runOnlyForDeploymentPostprocessing = 0;
71 | };
72 | /* End PBXFrameworksBuildPhase section */
73 |
74 | /* Begin PBXGroup section */
75 | 5C4C6421264635BC00FC1D5D /* Classes */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 5C4C641926439CF900FC1D5D /* IMITextView.swift */,
79 | 5C4C6417263BF77400FC1D5D /* IMTextLayoutManager.swift */,
80 | 5C4C642E26463CD100FC1D5D /* IMTextViewConfiguration.swift */,
81 | 5C4C642C26463C3C00FC1D5D /* IMUITextView.swift */,
82 | );
83 | path = Classes;
84 | sourceTree = "";
85 | };
86 | 5C4C643426464D6F00FC1D5D /* Deployment */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 5C4C643026464D1E00FC1D5D /* README.md */,
90 | 5C4C643126464D1E00FC1D5D /* Package.swift */,
91 | 5C4C643526464D9D00FC1D5D /* IMITextView.podspec */,
92 | 5C4C6436264650D300FC1D5D /* LICENSE */,
93 | );
94 | name = Deployment;
95 | sourceTree = "";
96 | };
97 | 5C92F0ED263A52030058FD6B = {
98 | isa = PBXGroup;
99 | children = (
100 | 5C4C643426464D6F00FC1D5D /* Deployment */,
101 | 5C92F0F9263A52030058FD6B /* IMITextView */,
102 | 5C92F108263A52270058FD6B /* Demon */,
103 | 5C92F0F8263A52030058FD6B /* Products */,
104 | );
105 | sourceTree = "";
106 | };
107 | 5C92F0F8263A52030058FD6B /* Products */ = {
108 | isa = PBXGroup;
109 | children = (
110 | 5C92F0F7263A52030058FD6B /* IMITextView.framework */,
111 | 5C92F107263A52270058FD6B /* Demon.app */,
112 | );
113 | name = Products;
114 | sourceTree = "";
115 | };
116 | 5C92F0F9263A52030058FD6B /* IMITextView */ = {
117 | isa = PBXGroup;
118 | children = (
119 | 5C4C6421264635BC00FC1D5D /* Classes */,
120 | 5C92F0FA263A52030058FD6B /* IMITextView.h */,
121 | 5C92F0FB263A52030058FD6B /* Info.plist */,
122 | );
123 | path = IMITextView;
124 | sourceTree = "";
125 | };
126 | 5C92F108263A52270058FD6B /* Demon */ = {
127 | isa = PBXGroup;
128 | children = (
129 | 5C92F109263A52270058FD6B /* AppDelegate.swift */,
130 | 5C92F10D263A52270058FD6B /* ViewController.swift */,
131 | 5C92F10F263A52270058FD6B /* Main.storyboard */,
132 | 5C92F112263A52280058FD6B /* Assets.xcassets */,
133 | 5C92F114263A52280058FD6B /* LaunchScreen.storyboard */,
134 | 5C92F117263A52280058FD6B /* Info.plist */,
135 | );
136 | path = Demon;
137 | sourceTree = "";
138 | };
139 | /* End PBXGroup section */
140 |
141 | /* Begin PBXHeadersBuildPhase section */
142 | 5C92F0F2263A52030058FD6B /* Headers */ = {
143 | isa = PBXHeadersBuildPhase;
144 | buildActionMask = 2147483647;
145 | files = (
146 | 5C92F0FC263A52030058FD6B /* IMITextView.h in Headers */,
147 | );
148 | runOnlyForDeploymentPostprocessing = 0;
149 | };
150 | /* End PBXHeadersBuildPhase section */
151 |
152 | /* Begin PBXNativeTarget section */
153 | 5C92F0F6263A52030058FD6B /* IMITextView */ = {
154 | isa = PBXNativeTarget;
155 | buildConfigurationList = 5C92F0FF263A52030058FD6B /* Build configuration list for PBXNativeTarget "IMITextView" */;
156 | buildPhases = (
157 | 5C92F0F2263A52030058FD6B /* Headers */,
158 | 5C92F0F3263A52030058FD6B /* Sources */,
159 | 5C92F0F4263A52030058FD6B /* Frameworks */,
160 | 5C92F0F5263A52030058FD6B /* Resources */,
161 | );
162 | buildRules = (
163 | );
164 | dependencies = (
165 | );
166 | name = IMITextView;
167 | productName = IMITextView;
168 | productReference = 5C92F0F7263A52030058FD6B /* IMITextView.framework */;
169 | productType = "com.apple.product-type.framework";
170 | };
171 | 5C92F106263A52270058FD6B /* Demon */ = {
172 | isa = PBXNativeTarget;
173 | buildConfigurationList = 5C92F118263A52280058FD6B /* Build configuration list for PBXNativeTarget "Demon" */;
174 | buildPhases = (
175 | 5C92F103263A52270058FD6B /* Sources */,
176 | 5C92F104263A52270058FD6B /* Frameworks */,
177 | 5C92F105263A52270058FD6B /* Resources */,
178 | 5C92F123263A52DA0058FD6B /* CopyFiles */,
179 | );
180 | buildRules = (
181 | );
182 | dependencies = (
183 | );
184 | name = Demon;
185 | productName = Demon;
186 | productReference = 5C92F107263A52270058FD6B /* Demon.app */;
187 | productType = "com.apple.product-type.application";
188 | };
189 | /* End PBXNativeTarget section */
190 |
191 | /* Begin PBXProject section */
192 | 5C92F0EE263A52030058FD6B /* Project object */ = {
193 | isa = PBXProject;
194 | attributes = {
195 | LastSwiftUpdateCheck = 1240;
196 | LastUpgradeCheck = 1240;
197 | TargetAttributes = {
198 | 5C92F0F6263A52030058FD6B = {
199 | CreatedOnToolsVersion = 12.4;
200 | };
201 | 5C92F106263A52270058FD6B = {
202 | CreatedOnToolsVersion = 12.4;
203 | };
204 | };
205 | };
206 | buildConfigurationList = 5C92F0F1263A52030058FD6B /* Build configuration list for PBXProject "IMITextView" */;
207 | compatibilityVersion = "Xcode 9.3";
208 | developmentRegion = en;
209 | hasScannedForEncodings = 0;
210 | knownRegions = (
211 | en,
212 | Base,
213 | );
214 | mainGroup = 5C92F0ED263A52030058FD6B;
215 | productRefGroup = 5C92F0F8263A52030058FD6B /* Products */;
216 | projectDirPath = "";
217 | projectRoot = "";
218 | targets = (
219 | 5C92F0F6263A52030058FD6B /* IMITextView */,
220 | 5C92F106263A52270058FD6B /* Demon */,
221 | );
222 | };
223 | /* End PBXProject section */
224 |
225 | /* Begin PBXResourcesBuildPhase section */
226 | 5C92F0F5263A52030058FD6B /* Resources */ = {
227 | isa = PBXResourcesBuildPhase;
228 | buildActionMask = 2147483647;
229 | files = (
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | 5C92F105263A52270058FD6B /* Resources */ = {
234 | isa = PBXResourcesBuildPhase;
235 | buildActionMask = 2147483647;
236 | files = (
237 | 5C92F116263A52280058FD6B /* LaunchScreen.storyboard in Resources */,
238 | 5C92F113263A52280058FD6B /* Assets.xcassets in Resources */,
239 | 5C92F111263A52270058FD6B /* Main.storyboard in Resources */,
240 | );
241 | runOnlyForDeploymentPostprocessing = 0;
242 | };
243 | /* End PBXResourcesBuildPhase section */
244 |
245 | /* Begin PBXSourcesBuildPhase section */
246 | 5C92F0F3263A52030058FD6B /* Sources */ = {
247 | isa = PBXSourcesBuildPhase;
248 | buildActionMask = 2147483647;
249 | files = (
250 | 5C4C641F2646359C00FC1D5D /* IMTextLayoutManager.swift in Sources */,
251 | 5C4C642D26463C3C00FC1D5D /* IMUITextView.swift in Sources */,
252 | 5C4C642F26463CD100FC1D5D /* IMTextViewConfiguration.swift in Sources */,
253 | 5C4C64202646359C00FC1D5D /* IMITextView.swift in Sources */,
254 | );
255 | runOnlyForDeploymentPostprocessing = 0;
256 | };
257 | 5C92F103263A52270058FD6B /* Sources */ = {
258 | isa = PBXSourcesBuildPhase;
259 | buildActionMask = 2147483647;
260 | files = (
261 | 5C92F10E263A52270058FD6B /* ViewController.swift in Sources */,
262 | 5C92F10A263A52270058FD6B /* AppDelegate.swift in Sources */,
263 | );
264 | runOnlyForDeploymentPostprocessing = 0;
265 | };
266 | /* End PBXSourcesBuildPhase section */
267 |
268 | /* Begin PBXVariantGroup section */
269 | 5C92F10F263A52270058FD6B /* Main.storyboard */ = {
270 | isa = PBXVariantGroup;
271 | children = (
272 | 5C92F110263A52270058FD6B /* Base */,
273 | );
274 | name = Main.storyboard;
275 | sourceTree = "";
276 | };
277 | 5C92F114263A52280058FD6B /* LaunchScreen.storyboard */ = {
278 | isa = PBXVariantGroup;
279 | children = (
280 | 5C92F115263A52280058FD6B /* Base */,
281 | );
282 | name = LaunchScreen.storyboard;
283 | sourceTree = "";
284 | };
285 | /* End PBXVariantGroup section */
286 |
287 | /* Begin XCBuildConfiguration section */
288 | 5C92F0FD263A52030058FD6B /* Debug */ = {
289 | isa = XCBuildConfiguration;
290 | buildSettings = {
291 | ALWAYS_SEARCH_USER_PATHS = NO;
292 | CLANG_ANALYZER_NONNULL = YES;
293 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
294 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
295 | CLANG_CXX_LIBRARY = "libc++";
296 | CLANG_ENABLE_MODULES = YES;
297 | CLANG_ENABLE_OBJC_ARC = YES;
298 | CLANG_ENABLE_OBJC_WEAK = YES;
299 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
300 | CLANG_WARN_BOOL_CONVERSION = YES;
301 | CLANG_WARN_COMMA = YES;
302 | CLANG_WARN_CONSTANT_CONVERSION = YES;
303 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
304 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
305 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
306 | CLANG_WARN_EMPTY_BODY = YES;
307 | CLANG_WARN_ENUM_CONVERSION = YES;
308 | CLANG_WARN_INFINITE_RECURSION = YES;
309 | CLANG_WARN_INT_CONVERSION = YES;
310 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
311 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
312 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
313 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
314 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
315 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
316 | CLANG_WARN_STRICT_PROTOTYPES = YES;
317 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
318 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
319 | CLANG_WARN_UNREACHABLE_CODE = YES;
320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
321 | COPY_PHASE_STRIP = NO;
322 | CURRENT_PROJECT_VERSION = 1;
323 | DEBUG_INFORMATION_FORMAT = dwarf;
324 | ENABLE_STRICT_OBJC_MSGSEND = YES;
325 | ENABLE_TESTABILITY = YES;
326 | GCC_C_LANGUAGE_STANDARD = gnu11;
327 | GCC_DYNAMIC_NO_PIC = NO;
328 | GCC_NO_COMMON_BLOCKS = YES;
329 | GCC_OPTIMIZATION_LEVEL = 0;
330 | GCC_PREPROCESSOR_DEFINITIONS = (
331 | "DEBUG=1",
332 | "$(inherited)",
333 | );
334 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
336 | GCC_WARN_UNDECLARED_SELECTOR = YES;
337 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
338 | GCC_WARN_UNUSED_FUNCTION = YES;
339 | GCC_WARN_UNUSED_VARIABLE = YES;
340 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
341 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
342 | MTL_FAST_MATH = YES;
343 | ONLY_ACTIVE_ARCH = YES;
344 | SDKROOT = iphoneos;
345 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
346 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
347 | VERSIONING_SYSTEM = "apple-generic";
348 | VERSION_INFO_PREFIX = "";
349 | };
350 | name = Debug;
351 | };
352 | 5C92F0FE263A52030058FD6B /* Release */ = {
353 | isa = XCBuildConfiguration;
354 | buildSettings = {
355 | ALWAYS_SEARCH_USER_PATHS = NO;
356 | CLANG_ANALYZER_NONNULL = YES;
357 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
359 | CLANG_CXX_LIBRARY = "libc++";
360 | CLANG_ENABLE_MODULES = YES;
361 | CLANG_ENABLE_OBJC_ARC = YES;
362 | CLANG_ENABLE_OBJC_WEAK = YES;
363 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
364 | CLANG_WARN_BOOL_CONVERSION = YES;
365 | CLANG_WARN_COMMA = YES;
366 | CLANG_WARN_CONSTANT_CONVERSION = YES;
367 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
368 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
369 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
370 | CLANG_WARN_EMPTY_BODY = YES;
371 | CLANG_WARN_ENUM_CONVERSION = YES;
372 | CLANG_WARN_INFINITE_RECURSION = YES;
373 | CLANG_WARN_INT_CONVERSION = YES;
374 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
375 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
376 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
377 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
378 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
379 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
380 | CLANG_WARN_STRICT_PROTOTYPES = YES;
381 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
382 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
383 | CLANG_WARN_UNREACHABLE_CODE = YES;
384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
385 | COPY_PHASE_STRIP = NO;
386 | CURRENT_PROJECT_VERSION = 1;
387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
388 | ENABLE_NS_ASSERTIONS = NO;
389 | ENABLE_STRICT_OBJC_MSGSEND = YES;
390 | GCC_C_LANGUAGE_STANDARD = gnu11;
391 | GCC_NO_COMMON_BLOCKS = YES;
392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
394 | GCC_WARN_UNDECLARED_SELECTOR = YES;
395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
396 | GCC_WARN_UNUSED_FUNCTION = YES;
397 | GCC_WARN_UNUSED_VARIABLE = YES;
398 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
399 | MTL_ENABLE_DEBUG_INFO = NO;
400 | MTL_FAST_MATH = YES;
401 | SDKROOT = iphoneos;
402 | SWIFT_COMPILATION_MODE = wholemodule;
403 | SWIFT_OPTIMIZATION_LEVEL = "-O";
404 | VALIDATE_PRODUCT = YES;
405 | VERSIONING_SYSTEM = "apple-generic";
406 | VERSION_INFO_PREFIX = "";
407 | };
408 | name = Release;
409 | };
410 | 5C92F100263A52030058FD6B /* Debug */ = {
411 | isa = XCBuildConfiguration;
412 | buildSettings = {
413 | CODE_SIGN_STYLE = Automatic;
414 | CURRENT_PROJECT_VERSION = 202105072030;
415 | DEFINES_MODULE = YES;
416 | DYLIB_COMPATIBILITY_VERSION = 1;
417 | DYLIB_CURRENT_VERSION = 1;
418 | DYLIB_INSTALL_NAME_BASE = "@rpath";
419 | INFOPLIST_FILE = IMITextView/Info.plist;
420 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
421 | LD_RUNPATH_SEARCH_PATHS = (
422 | "$(inherited)",
423 | "@executable_path/Frameworks",
424 | "@loader_path/Frameworks",
425 | );
426 | MARKETING_VERSION = 0.0.1;
427 | PRODUCT_BUNDLE_IDENTIFIER = com.immortal.IMITextView;
428 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
429 | SKIP_INSTALL = YES;
430 | SWIFT_VERSION = 5.0;
431 | TARGETED_DEVICE_FAMILY = "1,2";
432 | };
433 | name = Debug;
434 | };
435 | 5C92F101263A52030058FD6B /* Release */ = {
436 | isa = XCBuildConfiguration;
437 | buildSettings = {
438 | CODE_SIGN_STYLE = Automatic;
439 | CURRENT_PROJECT_VERSION = 202105072030;
440 | DEFINES_MODULE = YES;
441 | DYLIB_COMPATIBILITY_VERSION = 1;
442 | DYLIB_CURRENT_VERSION = 1;
443 | DYLIB_INSTALL_NAME_BASE = "@rpath";
444 | INFOPLIST_FILE = IMITextView/Info.plist;
445 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
446 | LD_RUNPATH_SEARCH_PATHS = (
447 | "$(inherited)",
448 | "@executable_path/Frameworks",
449 | "@loader_path/Frameworks",
450 | );
451 | MARKETING_VERSION = 0.0.1;
452 | PRODUCT_BUNDLE_IDENTIFIER = com.immortal.IMITextView;
453 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
454 | SKIP_INSTALL = YES;
455 | SWIFT_VERSION = 5.0;
456 | TARGETED_DEVICE_FAMILY = "1,2";
457 | };
458 | name = Release;
459 | };
460 | 5C92F119263A52280058FD6B /* Debug */ = {
461 | isa = XCBuildConfiguration;
462 | buildSettings = {
463 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
465 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
466 | CODE_SIGN_STYLE = Automatic;
467 | DEVELOPMENT_TEAM = HZ6KU6RMNJ;
468 | INFOPLIST_FILE = Demon/Info.plist;
469 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
470 | LD_RUNPATH_SEARCH_PATHS = (
471 | "$(inherited)",
472 | "@executable_path/Frameworks",
473 | );
474 | PRODUCT_BUNDLE_IDENTIFIER = com.immortal.Demon;
475 | PRODUCT_NAME = "$(TARGET_NAME)";
476 | SWIFT_VERSION = 5.0;
477 | TARGETED_DEVICE_FAMILY = "1,2";
478 | };
479 | name = Debug;
480 | };
481 | 5C92F11A263A52280058FD6B /* Release */ = {
482 | isa = XCBuildConfiguration;
483 | buildSettings = {
484 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
485 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
486 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
487 | CODE_SIGN_STYLE = Automatic;
488 | DEVELOPMENT_TEAM = HZ6KU6RMNJ;
489 | INFOPLIST_FILE = Demon/Info.plist;
490 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
491 | LD_RUNPATH_SEARCH_PATHS = (
492 | "$(inherited)",
493 | "@executable_path/Frameworks",
494 | );
495 | PRODUCT_BUNDLE_IDENTIFIER = com.immortal.Demon;
496 | PRODUCT_NAME = "$(TARGET_NAME)";
497 | SWIFT_VERSION = 5.0;
498 | TARGETED_DEVICE_FAMILY = "1,2";
499 | };
500 | name = Release;
501 | };
502 | /* End XCBuildConfiguration section */
503 |
504 | /* Begin XCConfigurationList section */
505 | 5C92F0F1263A52030058FD6B /* Build configuration list for PBXProject "IMITextView" */ = {
506 | isa = XCConfigurationList;
507 | buildConfigurations = (
508 | 5C92F0FD263A52030058FD6B /* Debug */,
509 | 5C92F0FE263A52030058FD6B /* Release */,
510 | );
511 | defaultConfigurationIsVisible = 0;
512 | defaultConfigurationName = Release;
513 | };
514 | 5C92F0FF263A52030058FD6B /* Build configuration list for PBXNativeTarget "IMITextView" */ = {
515 | isa = XCConfigurationList;
516 | buildConfigurations = (
517 | 5C92F100263A52030058FD6B /* Debug */,
518 | 5C92F101263A52030058FD6B /* Release */,
519 | );
520 | defaultConfigurationIsVisible = 0;
521 | defaultConfigurationName = Release;
522 | };
523 | 5C92F118263A52280058FD6B /* Build configuration list for PBXNativeTarget "Demon" */ = {
524 | isa = XCConfigurationList;
525 | buildConfigurations = (
526 | 5C92F119263A52280058FD6B /* Debug */,
527 | 5C92F11A263A52280058FD6B /* Release */,
528 | );
529 | defaultConfigurationIsVisible = 0;
530 | defaultConfigurationName = Release;
531 | };
532 | /* End XCConfigurationList section */
533 | };
534 | rootObject = 5C92F0EE263A52030058FD6B /* Project object */;
535 | }
536 |
--------------------------------------------------------------------------------
/IMITextView.xcodeproj/xcshareddata/xcschemes/Demon.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/IMITextView/Classes/IMITextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IMITextView.swift
3 | // IMITextView
4 | //
5 | // Created by immortal on 2021/5/6
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A scrollable, multiline text region.
12 | @available(iOS 11.0, *)
13 | public class IMITextView: UIScrollView {
14 |
15 | private let storage = NSTextStorage()
16 |
17 | private let layoutManager: IMTextLayoutManager = {
18 | let layoutManager = IMTextLayoutManager()
19 | layoutManager.allowsNonContiguousLayout = false
20 | layoutManager.usesFontLeading = false
21 | if #available(iOS 12.0, *) {
22 | layoutManager.limitsLayoutForSuspiciousContents = false
23 | }
24 | return layoutManager
25 | }()
26 |
27 | private let container: NSTextContainer = {
28 | let container = NSTextContainer(size: .zero)
29 | container.widthTracksTextView = true
30 | container.heightTracksTextView = true
31 | container.lineFragmentPadding = .zero
32 | return container
33 | }()
34 |
35 | public private(set) lazy var textView: UITextView = {
36 | let textView = IMUITextView(frame: .zero, textContainer: container)
37 | textView.translatesAutoresizingMaskIntoConstraints = false
38 | textView.backgroundColor = .clear
39 | textView.contentInset = .zero
40 | textView.clipsToBounds = false
41 | textView.textContainerInset = .init(top: 12.0, left: 24.0, bottom: 12.0, right: 24.0)
42 | textView.contentInsetAdjustmentBehavior = .never
43 | textView.isEditable = true
44 | textView.panGestureRecognizer.isEnabled = false
45 | textView.contentScaleFactor = UIScreen.main.scale
46 | // isScrollEnabled must be false,otherwise the background drawing will show an exception
47 | textView.isScrollEnabled = false
48 | return textView
49 | }()
50 |
51 | /// Whether is appending nwewline
52 | private var isAppendingNewline: Bool = false
53 |
54 | /// Configuration
55 | public var configuration: IMTextViewConfiguration = IMTextViewConfiguration() {
56 | didSet { apply(configuration) }
57 | }
58 |
59 | public override init(frame: CGRect) {
60 | super.init(frame: frame)
61 | initView()
62 | }
63 |
64 | public required init?(coder: NSCoder) {
65 | super.init(coder: coder)
66 | }
67 |
68 | private func initView() {
69 | layoutManager.addTextContainer(container)
70 | storage.addLayoutManager(layoutManager)
71 | loadSubviews()
72 | textView.delegate = self
73 | showsVerticalScrollIndicator = false
74 | showsHorizontalScrollIndicator = false
75 | alwaysBounceVertical = false
76 | backgroundColor = .clear
77 | apply(configuration)
78 | }
79 |
80 | private func loadSubviews() {
81 | addSubview(textView)
82 | let topConstraint = textView.topAnchor.constraint(greaterThanOrEqualTo: contentLayoutGuide.topAnchor)
83 | topConstraint.priority = .defaultHigh
84 | let centerYConstraint = textView.centerYAnchor.anchorWithOffset(to: contentLayoutGuide.topAnchor).constraint(equalTo: heightAnchor, multiplier: -0.5)
85 | centerYConstraint.priority = .defaultLow
86 | NSLayoutConstraint.activate([
87 | textView.widthAnchor.constraint(equalTo: widthAnchor),
88 | centerYConstraint,
89 | topConstraint,
90 | textView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
91 | textView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor),
92 | textView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor)
93 | ])
94 | }
95 |
96 | private func apply(_ configuration: IMTextViewConfiguration) {
97 | layoutManager.isCanDrawGlyphsOutsideStroke = configuration.isStrokeOuter
98 |
99 | layoutManager.lineBackgroundOptions = configuration.lineBackgroundOptions
100 | layoutManager.lineHeightPercentageForCornerRadius = configuration.lineHeightPercentageForCornerRadius
101 | layoutManager.lineBackgroundInset = configuration.lineBackgroundInset
102 | layoutManager.lineBackgroundColor = configuration.lineBackgroundColor
103 | layoutManager.lineBoderWidth = configuration.lineBoderWidth
104 | layoutManager.lineBoderColor = configuration.lineBoderColor
105 |
106 | textView.textContainerInset = configuration.lineBackgroundInset
107 |
108 | textView.typingAttributes[.strokeWidth] = configuration.strokeWidth * textView.contentScaleFactor
109 | textView.typingAttributes[.strokeColor] = configuration.strokeColor
110 | textView.typingAttributes[.kern] = configuration.kern * textView.contentScaleFactor
111 |
112 | textView.font = configuration.font
113 | textView.textColor = configuration.textColor
114 | textView.textAlignment = configuration.textAlignment.alignment
115 |
116 | let stringRange = NSRange(location: 0, length: storage.mutableString.length)
117 | if stringRange.length > 0 {
118 | storage.addAttributes(textView.typingAttributes, range: stringRange)
119 | }
120 | textView.setNeedsDisplay()
121 | }
122 |
123 | public var text: String {
124 | get { textView.text ?? "" }
125 | set { textView.text = newValue }
126 | }
127 |
128 | public var isEditable: Bool {
129 | get { textView.isEditable }
130 | set { textView.isEditable = newValue }
131 | }
132 |
133 | // MARK: - FirstResponder
134 |
135 | @discardableResult
136 | public override func becomeFirstResponder() -> Bool {
137 | textView.becomeFirstResponder()
138 | }
139 |
140 | public override var canBecomeFirstResponder: Bool {
141 | textView.canBecomeFirstResponder
142 | }
143 |
144 | public override var canResignFirstResponder: Bool {
145 | textView.canResignFirstResponder
146 | }
147 |
148 | @discardableResult
149 | public override func resignFirstResponder() -> Bool {
150 | textView.resignFirstResponder()
151 | }
152 |
153 | public override var isFirstResponder: Bool {
154 | textView.isFirstResponder
155 | }
156 | }
157 |
158 | extension IMITextView: UITextViewDelegate {
159 |
160 | /// Auto scroll to current caret
161 | public func textViewDidChange(_ textView: UITextView) {
162 | textView.setNeedsDisplay()
163 | if isAppendingNewline {
164 | isAppendingNewline = false
165 | if let font = textView.attributedText.attribute(.font, at: 0, effectiveRange: nil) as? UIFont {
166 | contentOffset.y += font.lineHeight + layoutManager.lineBackgroundInset.top + layoutManager.lineBackgroundInset.bottom
167 | }
168 | }
169 | }
170 |
171 | public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
172 | if textView.text.isEmpty && text.isEmpty {
173 | return false
174 | }
175 | isAppendingNewline = text.hasSuffix("\n")
176 | return true
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/IMITextView/Classes/IMTextLayoutManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IMTextLayoutManager.swift
3 | // IMITextView
4 | //
5 | // Created by immortal on 2021/4/30
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Text layout manager
12 | public class IMTextLayoutManager: NSLayoutManager {
13 |
14 | /// Line background options
15 | public struct LineBackgroundOptions: OptionSet {
16 |
17 | public let rawValue: UInt
18 |
19 | public init(rawValue: UInt) {
20 | self.rawValue = rawValue
21 | }
22 |
23 | /// Fill background with color
24 | public static var fill: Self {
25 | Self.init(rawValue: 1)
26 | }
27 |
28 | /// Stroke background boder
29 | public static var boder: Self {
30 | Self.init(rawValue: 2)
31 | }
32 | }
33 |
34 | /// Line background options
35 | public var lineBackgroundOptions: LineBackgroundOptions = []
36 |
37 | public override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
38 | super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
39 | guard !lineBackgroundOptions.isEmpty,
40 | let textStorage = textStorage else { return }
41 | let characterRange = NSRange(location: 0, length: textStorage.mutableString.length)
42 | guard characterRange.length > 0 else { return }
43 | let lineRects = lineRects(forGlyphRange: characterRange)
44 | if lineBackgroundOptions.contains(.boder) {
45 | strokeBackgroundBoder(lineRects, at: origin, boderWidth: lineBoderWidth, boderColor: lineBoderColor)
46 | }
47 | if lineBackgroundOptions.contains(.fill) {
48 | fillBackground(lineRects, at: origin, color: lineBackgroundColor)
49 | }
50 | }
51 |
52 | public override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
53 | // Fix error rect for lineBackgroundInset.bottom
54 | let containerOrigin = CGPoint(x: origin.x, y: origin.y - lineBackgroundInset.bottom)
55 |
56 | super.drawGlyphs(forGlyphRange: glyphsToShow, at: containerOrigin)
57 | strokeOuter(forGlyphRange: glyphsToShow, at: containerOrigin)
58 | }
59 |
60 | // MARK: - Prepare
61 |
62 | /// Line rects
63 | private func lineRects(forGlyphRange glyphsToShow: NSRange) -> [CGRect] {
64 | var rects: [CGRect] = []
65 | let characterRange = self.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
66 | let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
67 | let inset = lineBackgroundInset
68 |
69 | enumerateLineFragments(forGlyphRange: glyphRange) { [unowned self] (rect, usedRect, textContainer, glyphRange, stop) in
70 |
71 | // Ignore blank newlines
72 | if propertyForGlyph(at: glyphRange.location) == .controlCharacter {
73 | return
74 | }
75 |
76 | // Fix error rect for whitespace
77 | if let lineText = textContainer.layoutManager?.textStorage?.attributedSubstring(from: glyphRange).string.replacingOccurrences(of: " ", with: ""),
78 | lineText.isEmpty || lineText == "\n",
79 | let paragraphStyle = textContainer.layoutManager?.textStorage?.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle,
80 | paragraphStyle.alignment == .center {
81 | rects.append(CGRect(x: usedRect.origin.x - inset.left - usedRect.size.width * 0.5,
82 | y: usedRect.origin.y - inset.top - inset.bottom,
83 | width: usedRect.size.width + inset.left + inset.right,
84 | height: usedRect.size.height + inset.top + inset.bottom))
85 | return
86 | }
87 |
88 | // Append
89 | rects.append(CGRect(x: usedRect.origin.x - inset.left,
90 | y: usedRect.origin.y - inset.top - inset.bottom, // Fix error rect for lineBackgroundInset.bottom
91 | width: usedRect.size.width + inset.left + inset.right,
92 | height: usedRect.size.height + inset.top + inset.bottom))
93 | }
94 | optimizeLineRects(&rects)
95 | return rects
96 | }
97 |
98 | /// Optimize line rects for fixing side line contact
99 | private func optimizeLineRects(_ lineRects: inout [CGRect]) {
100 | guard lineRects.count > 1 else { return }
101 |
102 | func processIndex(_ index: Int) {
103 | guard index > 0, index < lineRects.count else { return }
104 | let lastLineRect = lineRects[index - 1]
105 | let currentLineRect = lineRects[index]
106 | guard lastLineRect.maxY >= currentLineRect.minY else { return }
107 |
108 | let cornerRadius = currentLineRect.size.height * lineHeightPercentageForCornerRadius
109 |
110 | // Current line rect top side points
111 | let currentLineRectTopLeft = currentLineRect.origin
112 | let currentLineRectTopRight = CGPoint(x: currentLineRect.maxX, y: currentLineRect.minY)
113 |
114 | // Last line rect bottom side points
115 | let lastLineRectBottomLeft = CGPoint(x: lastLineRect.minX, y: lastLineRect.maxY)
116 | let lastLineRectBottomRight = CGPoint(x: lastLineRect.maxX, y: lastLineRect.maxY)
117 |
118 | let leftRadius = (currentLineRectTopLeft.x - lastLineRectBottomLeft.x) * 0.5
119 | let rightRadius = (currentLineRectTopRight.x - lastLineRectBottomRight.x) * 0.5
120 |
121 | if (leftRadius > 0.0 && abs(leftRadius) < cornerRadius) || (rightRadius < 0.0 && abs(rightRadius) < cornerRadius) {
122 | lineRects[index] = CGRect(x: lastLineRect.minX, y: currentLineRect.minY, width: lastLineRect.width, height: currentLineRect.height)
123 | processIndex(index + 1)
124 | } else if (leftRadius < 0.0 && abs(leftRadius) < cornerRadius) || (rightRadius > 0.0 && abs(rightRadius) < cornerRadius) {
125 | lineRects[index - 1] = CGRect(x: currentLineRect.minX, y: lastLineRect.minY, width: currentLineRect.width, height: lastLineRect.height)
126 | processIndex(index - 1)
127 | }
128 | }
129 |
130 | for index in 1.. 0, lastLineRect.maxY >= $0.element.minY {
162 |
163 | // Current line rect top side points
164 | let currentLineRectTopLeft = $0.element.origin
165 | let currentLineRectTopRight = CGPoint(x: $0.element.maxX, y: $0.element.minY)
166 |
167 | // Last line rect bottom side points
168 | let lastLineRectBottomLeft = CGPoint(x: lastLineRect.minX, y: lastLineRect.maxY)
169 | let lastLineRectBottomRight = CGPoint(x: lastLineRect.maxX, y: lastLineRect.maxY)
170 |
171 | let leftRadius = (currentLineRectTopLeft.x - lastLineRectBottomLeft.x) * 0.5
172 | let rightRadius = (lastLineRectBottomRight.x - currentLineRectTopRight.x) * 0.5
173 |
174 | // Left corner
175 | if leftRadius >= cornerRadius { // Left corner inside
176 |
177 | // Fix current left corner
178 | let fixedPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: lastLineRectBottomLeft.y + cornerRadius ),
179 | radius: cornerRadius,
180 | startAngle: CGFloat.pi,
181 | endAngle: CGFloat.pi * 1.5,
182 | clockwise: true)
183 | fixedPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: lastLineRectBottomLeft.y))
184 | fixedPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: lastLineRectBottomLeft.y + cornerRadius))
185 | backgroundPath.append(fixedPath.reversing())
186 |
187 | // Show left corner
188 | let cornerPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopLeft.x - cornerRadius, y: lastLineRectBottomLeft.y + cornerRadius ),
189 | radius: cornerRadius,
190 | startAngle: CGFloat.pi * 1.5,
191 | endAngle: 0.0,
192 | clockwise: true)
193 | cornerPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: lastLineRectBottomLeft.y))
194 | cornerPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x - cornerRadius, y: lastLineRectBottomLeft.y))
195 | backgroundPath.append(cornerPath)
196 |
197 | } else if leftRadius <= -cornerRadius { // Left corner outside
198 |
199 | // Fix last left corner
200 | let fixedPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomLeft.x + cornerRadius, y: currentLineRectTopLeft.y - cornerRadius ),
201 | radius: cornerRadius,
202 | startAngle: CGFloat.pi * 0.5,
203 | endAngle: CGFloat.pi,
204 | clockwise: true)
205 | fixedPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x, y: currentLineRectTopLeft.y))
206 | fixedPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x + cornerRadius, y: currentLineRectTopLeft.y))
207 | backgroundPath.append(fixedPath.reversing())
208 |
209 | // Show left corner
210 | let cornerPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomLeft.x - cornerRadius, y: currentLineRectTopLeft.y - cornerRadius),
211 | radius: cornerRadius,
212 | startAngle: 0.0,
213 | endAngle: CGFloat.pi * 0.5,
214 | clockwise: true)
215 | cornerPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x, y: currentLineRectTopLeft.y))
216 | cornerPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x, y: currentLineRectTopLeft.y - cornerRadius))
217 | backgroundPath.append(cornerPath)
218 |
219 | } else if leftRadius == .zero { // Left corner equtal
220 |
221 | // Fix last left corner
222 | let fixedLastPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomLeft.x + cornerRadius, y: lastLineRectBottomLeft.y - cornerRadius),
223 | radius: cornerRadius,
224 | startAngle: CGFloat.pi * 0.5,
225 | endAngle: CGFloat.pi,
226 | clockwise: true)
227 | fixedLastPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x, y: lastLineRectBottomLeft.y))
228 | fixedLastPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x + cornerRadius, y: lastLineRectBottomLeft.y))
229 | backgroundPath.append(fixedLastPath.reversing())
230 |
231 | // Fix current left corner
232 | let fixedCurrentPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: currentLineRectTopLeft.y + cornerRadius),
233 | radius: cornerRadius,
234 | startAngle: CGFloat.pi,
235 | endAngle: CGFloat.pi * 1.5,
236 | clockwise: true)
237 | fixedCurrentPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: currentLineRectTopLeft.y))
238 | fixedCurrentPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: currentLineRectTopLeft.y + cornerRadius))
239 | backgroundPath.append(fixedCurrentPath.reversing())
240 | }
241 |
242 | // Right corner
243 | if (rightRadius >= cornerRadius) { // Right corner inside
244 |
245 | // Fix current right corner
246 | let fixedPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: lastLineRectBottomRight.y + cornerRadius ),
247 | radius: cornerRadius,
248 | startAngle: CGFloat.pi * 1.5,
249 | endAngle: 0.0,
250 | clockwise: true)
251 | fixedPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: lastLineRectBottomRight.y))
252 | fixedPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x - cornerRadius, y: lastLineRectBottomRight.y))
253 | backgroundPath.append(fixedPath.reversing())
254 |
255 | // Show right corner
256 | let cornerPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopRight.x + cornerRadius, y: lastLineRectBottomRight.y + cornerRadius ),
257 | radius: cornerRadius,
258 | startAngle: CGFloat.pi,
259 | endAngle: CGFloat.pi * 1.5,
260 | clockwise: true)
261 | cornerPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: lastLineRectBottomRight.y))
262 | cornerPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: lastLineRectBottomRight.y + cornerRadius))
263 | backgroundPath.append(cornerPath)
264 |
265 | } else if rightRadius <= -cornerRadius { // Right corner outside
266 |
267 | // Fix last right corner
268 | let fixedPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomRight.x - cornerRadius, y: currentLineRectTopRight.y - cornerRadius ),
269 | radius: cornerRadius,
270 | startAngle: 0.0,
271 | endAngle: CGFloat.pi * 0.5,
272 | clockwise: true)
273 | fixedPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: lastLineRectBottomRight.y))
274 | fixedPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: currentLineRectTopLeft.y - cornerRadius))
275 | backgroundPath.append(fixedPath.reversing())
276 |
277 | // Show right corner
278 | let cornerPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomRight.x + cornerRadius, y: currentLineRectTopRight.y - cornerRadius),
279 | radius: cornerRadius,
280 | startAngle: CGFloat.pi * 0.5,
281 | endAngle: CGFloat.pi,
282 | clockwise: true)
283 |
284 | cornerPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: currentLineRectTopRight.y))
285 | cornerPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x + cornerRadius, y: currentLineRectTopRight.y))
286 | backgroundPath.append(cornerPath)
287 |
288 |
289 | } else if rightRadius == .zero { // Right corner equtal
290 |
291 | // Fix last right corner
292 | let fixedLastPath = UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomRight.x - cornerRadius, y: lastLineRectBottomRight.y - cornerRadius),
293 | radius: cornerRadius,
294 | startAngle: 0.0,
295 | endAngle: CGFloat.pi * 0.5,
296 | clockwise: true)
297 | fixedLastPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: lastLineRectBottomRight.y))
298 | fixedLastPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: lastLineRectBottomRight.y - cornerRadius))
299 | backgroundPath.append(fixedLastPath.reversing())
300 |
301 | // Fix current right corner
302 | let fixedCurrentPath = UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: currentLineRectTopRight.y + cornerRadius),
303 | radius: cornerRadius,
304 | startAngle: CGFloat.pi * 1.5,
305 | endAngle: 0.0,
306 | clockwise: true)
307 | fixedCurrentPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: currentLineRectTopRight.y))
308 | fixedCurrentPath.addLine(to: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: currentLineRectTopRight.y))
309 | backgroundPath.append(fixedCurrentPath.reversing())
310 | }
311 | }
312 | lastLineRect = $0.element
313 | }
314 | backgroundPath.stroke()
315 | backgroundPath.fill()
316 |
317 | context.restoreGState()
318 | }
319 |
320 | // MARK: - Background Boder
321 |
322 | /// Scale of clear line width
323 | private let scaleOfClearLineWidth: CGFloat = 1.5
324 |
325 | /// Line boder width, default value is `2.0`
326 | public var lineBoderWidth: CGFloat = 2.0
327 |
328 | /// Line boder color, default value is `.white`
329 | public var lineBoderColor: UIColor = .white
330 |
331 | /// Stroke background boder
332 | private func strokeBackgroundBoder(_ lineRects: [CGRect], at origin: CGPoint, boderWidth: CGFloat, boderColor: UIColor) {
333 | guard let context = UIGraphicsGetCurrentContext() else { return }
334 | context.saveGState()
335 | context.translateBy(x: origin.x, y: origin.y)
336 |
337 | func setContext(_ context: CGContext, isClear: Bool) {
338 | if isClear {
339 | context.setBlendMode(.clear)
340 | UIColor.clear.setStroke()
341 | UIColor.clear.setFill()
342 | } else {
343 | context.setBlendMode(.normal)
344 | boderColor.setStroke()
345 | UIColor.clear.setFill()
346 | }
347 | }
348 |
349 | func strokePath(with context: CGContext,
350 | center: CGPoint,
351 | radius: CGFloat,
352 | startAngle: CGFloat,
353 | endAngle: CGFloat,
354 | clockwise: Bool) {
355 | setContext(context, isClear: false)
356 | let strokePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
357 | strokePath.lineWidth = boderWidth
358 | strokePath.lineCapStyle = .round
359 | strokePath.stroke()
360 | }
361 |
362 | func strokeBoder(_ rect: CGRect, cornerRadius: CGFloat) {
363 | setContext(context, isClear: false)
364 | let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
365 | path.lineWidth = boderWidth
366 | path.lineCapStyle = .round
367 | path.lineJoinStyle = .round
368 | path.stroke()
369 | }
370 |
371 | var lastLineRect: CGRect = .zero
372 | lineRects.enumerated().forEach {
373 | let cornerRadius = $0.element.height * lineHeightPercentageForCornerRadius
374 |
375 | if $0.offset > 0, lastLineRect.maxY >= $0.element.minY {
376 |
377 | // Current line rect top side points
378 | let currentLineRectTopLeft = $0.element.origin
379 | let currentLineRectTopRight = CGPoint(x: $0.element.maxX, y: $0.element.minY)
380 |
381 | // Last line rect bottom side points
382 | let lastLineRectBottomLeft = CGPoint(x: lastLineRect.minX, y: lastLineRect.maxY)
383 | let lastLineRectBottomRight = CGPoint(x: lastLineRect.maxX, y: lastLineRect.maxY)
384 |
385 | let leftRadius = (currentLineRectTopLeft.x - lastLineRectBottomLeft.x) * 0.5
386 | let rightRadius = (lastLineRectBottomRight.x - currentLineRectTopRight.x) * 0.5
387 |
388 | let centerX = (
389 | (currentLineRectTopLeft.x > lastLineRectBottomLeft.x ? currentLineRectTopLeft.x : lastLineRectBottomLeft.x) +
390 | (currentLineRectTopRight.x > lastLineRectBottomRight.x ? lastLineRectBottomRight.x : currentLineRectTopRight.x)
391 | ) * 0.5
392 |
393 | // Boder
394 | if leftRadius >= cornerRadius && rightRadius >= cornerRadius {
395 | strokeBoder(CGRect(x: $0.element.minX, y: lastLineRectBottomLeft.y, width: $0.element.width, height: $0.element.maxY - lastLineRectBottomLeft.y), cornerRadius: cornerRadius)
396 | } else {
397 | strokeBoder($0.element, cornerRadius: cornerRadius)
398 | }
399 |
400 | // Left corner
401 | if leftRadius >= cornerRadius { // Left corner inside
402 | setContext(context, isClear: true)
403 | let clearPath = UIBezierPath()
404 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: lastLineRectBottomLeft.y + cornerRadius),
405 | radius: cornerRadius,
406 | startAngle: CGFloat.pi,
407 | endAngle: CGFloat.pi * 1.5,
408 | clockwise: true))
409 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: lastLineRectBottomLeft.y))
410 | clearPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x - cornerRadius, y: lastLineRectBottomLeft.y))
411 |
412 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: currentLineRectTopLeft.y + cornerRadius),
413 | radius: cornerRadius,
414 | startAngle: CGFloat.pi,
415 | endAngle: CGFloat.pi * 1.5,
416 | clockwise: true))
417 | clearPath.move(to: CGPoint(x: centerX + boderWidth * 0.5, y: currentLineRectTopLeft.y))
418 | clearPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: currentLineRectTopLeft.y))
419 | clearPath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: currentLineRectTopLeft.y + cornerRadius * 2.0))
420 |
421 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
422 | clearPath.stroke()
423 |
424 | strokePath(with: context,
425 | center: CGPoint(x: currentLineRectTopLeft.x - cornerRadius, y: lastLineRectBottomLeft.y + cornerRadius),
426 | radius: cornerRadius,
427 | startAngle: CGFloat.pi * 1.5,
428 | endAngle: 0.0,
429 | clockwise: true)
430 |
431 | } else if leftRadius <= -cornerRadius { // Left corner outside
432 |
433 | setContext(context, isClear: true)
434 | let clearPath = UIBezierPath()
435 |
436 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomLeft.x + cornerRadius, y: lastLineRectBottomLeft.y - cornerRadius),
437 | radius: cornerRadius,
438 | startAngle: CGFloat.pi,
439 | endAngle: CGFloat.pi * 0.5,
440 | clockwise: false))
441 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: lastLineRectBottomLeft.y))
442 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x - cornerRadius, y: lastLineRectBottomLeft.y))
443 |
444 | clearPath.move(to: CGPoint(x: centerX + boderWidth * 0.5, y: currentLineRectTopLeft.y))
445 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x - cornerRadius, y: currentLineRectTopLeft.y))
446 |
447 | clearPath.move(to: CGPoint(x: lastLineRectBottomLeft.x, y: currentLineRectTopLeft.y - cornerRadius))
448 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomLeft.x, y: lastLineRectBottomLeft.y))
449 |
450 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
451 | clearPath.stroke()
452 |
453 | strokePath(with: context,
454 | center: CGPoint(x: lastLineRectBottomLeft.x - cornerRadius, y: currentLineRectTopLeft.y - cornerRadius),
455 | radius: cornerRadius,
456 | startAngle: CGFloat.pi * 0.5,
457 | endAngle: 0.0,
458 | clockwise: false)
459 |
460 | } else if leftRadius == .zero { // Left corner equtal
461 | setContext(context, isClear: true)
462 | let clearPath = UIBezierPath()
463 |
464 | // Last
465 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: currentLineRectTopLeft.y + cornerRadius),
466 | radius: cornerRadius,
467 | startAngle: CGFloat.pi,
468 | endAngle: CGFloat.pi * 1.5,
469 | clockwise: true)
470 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: currentLineRectTopLeft.y))
471 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: currentLineRectTopLeft.y - cornerRadius),
472 | radius: cornerRadius,
473 | startAngle: CGFloat.pi * 0.5,
474 | endAngle: CGFloat.pi,
475 | clockwise: true)
476 |
477 | // Current
478 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: lastLineRectBottomLeft.y + cornerRadius),
479 | radius: cornerRadius,
480 | startAngle: CGFloat.pi,
481 | endAngle: CGFloat.pi * 1.5,
482 | clockwise: true)
483 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: lastLineRectBottomLeft.y))
484 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopLeft.x + cornerRadius, y: lastLineRectBottomLeft.y - cornerRadius),
485 | radius: cornerRadius,
486 | startAngle: CGFloat.pi * 0.5,
487 | endAngle: CGFloat.pi,
488 | clockwise: true)
489 |
490 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
491 | clearPath.stroke()
492 |
493 | setContext(context, isClear: false)
494 | let strokePath = UIBezierPath()
495 | strokePath.move(to: CGPoint(x: currentLineRectTopLeft.x, y: currentLineRectTopLeft.y - cornerRadius))
496 | strokePath.addLine(to: CGPoint(x: currentLineRectTopLeft.x, y: lastLineRectBottomLeft.y + cornerRadius))
497 | strokePath.lineWidth = boderWidth
498 | strokePath.lineCapStyle = .round
499 | strokePath.stroke()
500 | }
501 |
502 | // Right corner
503 | if (rightRadius >= cornerRadius) { // Right corner inside
504 | setContext(context, isClear: true)
505 | let clearPath = UIBezierPath()
506 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: lastLineRectBottomRight.y + cornerRadius),
507 | radius: cornerRadius,
508 | startAngle: 0.0,
509 | endAngle: CGFloat.pi * 1.5,
510 | clockwise: false))
511 | clearPath.move(to: CGPoint(x: centerX - boderWidth * 0.5, y: lastLineRectBottomRight.y))
512 | clearPath.addLine(to: CGPoint(x: currentLineRectTopRight.x + cornerRadius, y: lastLineRectBottomRight.y))
513 |
514 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: currentLineRectTopRight.y + cornerRadius),
515 | radius: cornerRadius,
516 | startAngle: 0.0,
517 | endAngle: CGFloat.pi * 0.5,
518 | clockwise: false))
519 | clearPath.move(to: CGPoint(x: centerX - boderWidth * 0.5, y: currentLineRectTopRight.y))
520 | clearPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: currentLineRectTopRight.y))
521 | clearPath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: currentLineRectTopRight.y + cornerRadius * 2.0))
522 |
523 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
524 | clearPath.stroke()
525 |
526 | strokePath(with: context,
527 | center: CGPoint(x: currentLineRectTopRight.x + cornerRadius, y: lastLineRectBottomRight.y + cornerRadius),
528 | radius: cornerRadius,
529 | startAngle: CGFloat.pi * 1.5,
530 | endAngle: CGFloat.pi,
531 | clockwise: false)
532 | } else if rightRadius <= -cornerRadius { // Right corner outside
533 | setContext(context, isClear: true)
534 | let clearPath = UIBezierPath()
535 | clearPath.append(UIBezierPath(arcCenter: CGPoint(x: lastLineRectBottomRight.x - cornerRadius, y: lastLineRectBottomRight.y - cornerRadius),
536 | radius: cornerRadius,
537 | startAngle: 0.0,
538 | endAngle: CGFloat.pi * 0.5,
539 | clockwise: true))
540 | clearPath.addLine(to: CGPoint(x: centerX - boderWidth * 0.5, y: lastLineRectBottomRight.y))
541 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x + cornerRadius, y: lastLineRectBottomRight.y))
542 |
543 | clearPath.move(to: CGPoint(x: centerX - boderWidth * 0.5, y: currentLineRectTopRight.y))
544 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x + cornerRadius, y: currentLineRectTopRight.y))
545 |
546 | clearPath.move(to: CGPoint(x: lastLineRectBottomRight.x, y: currentLineRectTopRight.y - cornerRadius))
547 | clearPath.addLine(to: CGPoint(x: lastLineRectBottomRight.x, y: lastLineRectBottomRight.y))
548 |
549 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
550 | clearPath.stroke()
551 |
552 | strokePath(with: context,
553 | center: CGPoint(x: lastLineRectBottomRight.x + cornerRadius, y: currentLineRectTopRight.y - cornerRadius),
554 | radius: cornerRadius,
555 | startAngle: CGFloat.pi * 0.5,
556 | endAngle: CGFloat.pi,
557 | clockwise: true)
558 | } else if rightRadius == .zero { // Right corner equtal
559 | setContext(context, isClear: true)
560 | let clearPath = UIBezierPath()
561 |
562 | // Last
563 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: lastLineRectBottomRight.y + cornerRadius),
564 | radius: cornerRadius,
565 | startAngle: 0.0,
566 | endAngle: CGFloat.pi * 1.5,
567 | clockwise: false)
568 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: lastLineRectBottomRight.y))
569 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: lastLineRectBottomRight.y - cornerRadius),
570 | radius: cornerRadius,
571 | startAngle: CGFloat.pi * 0.5,
572 | endAngle: 0.0,
573 | clockwise: false)
574 |
575 | // Current
576 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: currentLineRectTopRight.y + cornerRadius),
577 | radius: cornerRadius,
578 | startAngle: 0.0,
579 | endAngle: CGFloat.pi * 1.5,
580 | clockwise: false)
581 | clearPath.addLine(to: CGPoint(x: centerX + boderWidth * 0.5, y: currentLineRectTopRight.y))
582 | clearPath.addArc(withCenter: CGPoint(x: currentLineRectTopRight.x - cornerRadius, y: currentLineRectTopRight.y - cornerRadius),
583 | radius: cornerRadius,
584 | startAngle: CGFloat.pi * 0.5,
585 | endAngle: 0.0,
586 | clockwise: false)
587 |
588 | clearPath.lineWidth = boderWidth * scaleOfClearLineWidth
589 | clearPath.stroke()
590 |
591 | setContext(context, isClear: false)
592 | let strokePath = UIBezierPath()
593 | strokePath.move(to: CGPoint(x: currentLineRectTopRight.x, y: currentLineRectTopRight.y - cornerRadius))
594 | strokePath.addLine(to: CGPoint(x: currentLineRectTopRight.x, y: lastLineRectBottomRight.y + cornerRadius))
595 | strokePath.lineWidth = boderWidth
596 | strokePath.lineCapStyle = .round
597 | strokePath.stroke()
598 | }
599 |
600 | } else {
601 | strokeBoder($0.element, cornerRadius: cornerRadius)
602 | }
603 | lastLineRect = $0.element
604 | }
605 |
606 | context.restoreGState()
607 | }
608 |
609 | // MARK: - Stokes
610 |
611 | /// Whether draw glyphs with outside stroke or not, default value is `true`
612 | public var isCanDrawGlyphsOutsideStroke: Bool = true
613 |
614 | /// Draw outer stokes
615 | private func strokeOuter(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
616 | guard isCanDrawGlyphsOutsideStroke,
617 | let context = UIGraphicsGetCurrentContext() else { return }
618 | context.saveGState()
619 | context.translateBy(x: origin.x, y: origin.y)
620 |
621 | /// draw glyphs outside stroke
622 | enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, textContainer, glyphRange, stop) in
623 | guard let textStorage = textContainer.layoutManager?.textStorage,
624 | let strokeWidth = textStorage.attribute(.strokeWidth, at: glyphRange.location, effectiveRange: nil),
625 | let strokeWidthF = Float(String(describing: strokeWidth)),
626 | strokeWidthF > 0.0 else {
627 | return
628 | }
629 | let attributedText = NSMutableAttributedString(attributedString: textStorage.attributedSubstring(from: glyphRange))
630 | attributedText.removeAttribute(.backgroundColor, range: NSRange(location: 0, length: glyphRange.length))
631 | attributedText.removeAttribute(.strokeWidth, range: NSRange(location: 0, length: glyphRange.length))
632 | attributedText.draw(at: usedRect.origin)
633 | }
634 |
635 | context.restoreGState()
636 | }
637 | }
638 |
--------------------------------------------------------------------------------
/IMITextView/Classes/IMTextViewConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IMTextViewConfiguration.swift
3 | // IMITextView
4 | //
5 | // Created by immortal on 2021/5/8
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// TextView configuration
12 | public struct IMTextViewConfiguration {
13 |
14 | /// Text alignment
15 | public enum TextAlignment: Int {
16 |
17 | /// Visually left aligned
18 | case left
19 |
20 | /// Visually center aligned
21 | case center
22 |
23 | /// Visually right aligned
24 | case right
25 |
26 | /// NSTextAlignment
27 | public var alignment: NSTextAlignment {
28 | switch self {
29 | case .left: return .left
30 | case .center: return .center
31 | case .right: return .right
32 | }
33 | }
34 | }
35 |
36 | /// Line background options
37 | public typealias LineBackgroundOptions = IMTextLayoutManager.LineBackgroundOptions
38 |
39 |
40 | /// Text alignment, default value is`TextAlignment.center`
41 | public var textAlignment: TextAlignment = .center
42 |
43 | /// Text color, default value is`UIColor.black`
44 | public var textColor: UIColor = .black
45 |
46 | /// Text font, default value is`UIFont.systemFont(ofSize: 30.0, weight: .bold)`
47 | public var font: UIFont = UIFont.systemFont(ofSize: 30.0, weight: .bold)
48 |
49 |
50 | /// Whether stroke outer, default value is`true`
51 | public var isStrokeOuter: Bool = true
52 |
53 | /// In percent of font point size, default value is`CGFloat.zero`: no stroke; positive for stroke alone, negative for stroke and fill
54 | public var strokeWidth: CGFloat = .zero
55 |
56 | /// Stroke color, default value is`UIColor.white`
57 | public var strokeColor: UIColor = .white
58 |
59 |
60 | /// Amount to modify default kerning. 0 means kerning is disabled. default value is`CGFloat.zero`
61 | public var kern: CGFloat = .zero
62 |
63 |
64 | /// Line background options
65 | public var lineBackgroundOptions: LineBackgroundOptions = []
66 |
67 | /// Line height percentage for cornerRadius, default value is`0.13`
68 | public var lineHeightPercentageForCornerRadius: CGFloat = 0.13
69 |
70 | /// Line background content inset, default value is`UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) `
71 | public var lineBackgroundInset: UIEdgeInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
72 |
73 | /// Line background color, default value is `.white`
74 | public var lineBackgroundColor: UIColor = .white
75 |
76 |
77 | /// Line boder width, default value is `2.0`
78 | public let lineBoderWidth: CGFloat = 2.0
79 |
80 | /// Line boder color, default value is `.white`
81 | public var lineBoderColor: UIColor = .white
82 | }
83 |
--------------------------------------------------------------------------------
/IMITextView/Classes/IMUITextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IMUITextView.swift
3 | // IMITextView
4 | //
5 | // Created by immortal on 2021/5/8
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A scrollable, multiline text region.
12 | class IMUITextView: UITextView {
13 |
14 | /// Ajust first rect
15 | override func firstRect(for range: UITextRange) -> CGRect {
16 | var firstRect = super.firstRect(for: range)
17 | if let layoutManager = layoutManager as? IMTextLayoutManager {
18 | // Fix error rect for lineBackgroundInset.bottom
19 | firstRect.origin.y -= layoutManager.lineBackgroundInset.bottom
20 |
21 | // Fix error rect for whitespace
22 | if textAlignment == .center {
23 | let glyphRange = layoutManager.glyphRange(forBoundingRect: firstRect, in: textContainer)
24 | let lineText = textStorage.attributedSubstring(from: glyphRange).string.replacingOccurrences(of: " ", with: "")
25 | if lineText.isEmpty {
26 | firstRect.origin.x -= firstRect.width * 0.5
27 | }
28 | }
29 | }
30 | return firstRect
31 | }
32 |
33 | /// Ajust caret position
34 | override func caretRect(for position: UITextPosition) -> CGRect {
35 | var caretRect = super.caretRect(for: position)
36 | if let layoutManager = layoutManager as? IMTextLayoutManager {
37 | // Fix error rect for lineBackgroundInset.bottom
38 | caretRect.origin.y -= layoutManager.lineBackgroundInset.bottom
39 |
40 | // Fix error rect for whitespace
41 | if textAlignment == .center {
42 | let glyphIndex = layoutManager.glyphIndex(for: caretRect.origin, in: textContainer)
43 | let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: nil)
44 | let glyphRange = layoutManager.glyphRange(forBoundingRect: lineRect, in: textContainer)
45 | let lineText = textStorage.attributedSubstring(from: glyphRange).string.replacingOccurrences(of: " ", with: "")
46 | if lineText.isEmpty {
47 | caretRect.origin.x -= lineRect.width * 0.5
48 | }
49 | }
50 |
51 | }
52 | return caretRect
53 | }
54 |
55 | /// Ajust selection rects
56 | override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
57 | let selectionRects = super.selectionRects(for: range)
58 | if let layoutManager = layoutManager as? IMTextLayoutManager, layoutManager.lineBackgroundInset.bottom > 0 {
59 | return selectionRects.map({
60 | // // Fix error rect for whitespace
61 | // if textAlignment == .center {
62 | // let glyphRange = layoutManager.glyphRange(forBoundingRect: $0.rect, in: textContainer)
63 | // let lineText = textStorage.attributedSubstring(from: glyphRange).string.replacingOccurrences(of: " ", with: "")
64 | // if lineText.isEmpty {
65 | // let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: nil)
66 | // return IMUITextSelectionMutableRect($0, offset: CGPoint(x: -lineRect.width * 0.5, y: -layoutManager.lineBackgroundInset.bottom))
67 | // }
68 | // }
69 | // Fix error rect for lineBackgroundInset.bottom
70 | return IMUITextSelectionMutableRect($0, offset: CGPoint(x: 0.0, y: -layoutManager.lineBackgroundInset.bottom))
71 | })
72 | }
73 | return selectionRects
74 | }
75 |
76 | override var textContainerInset: UIEdgeInsets {
77 | set {
78 | if let layoutManager = layoutManager as? IMTextLayoutManager {
79 | // Fix error rect for lineBackgroundInset.bottom
80 | super.textContainerInset = UIEdgeInsets(top: newValue.top + layoutManager.lineBackgroundInset.bottom,
81 | left: newValue.left,
82 | bottom: newValue.bottom - layoutManager.lineBackgroundInset.bottom,
83 | right: newValue.right)
84 | } else {
85 | super.textContainerInset = newValue
86 | }
87 | }
88 | get {
89 | if let layoutManager = layoutManager as? IMTextLayoutManager {
90 | // Fix error rect for lineBackgroundInset.bottom
91 | return UIEdgeInsets(top: super.textContainerInset.top - layoutManager.lineBackgroundInset.bottom,
92 | left: super.textContainerInset.left,
93 | bottom: super.textContainerInset.bottom + layoutManager.lineBackgroundInset.bottom,
94 | right: super.textContainerInset.right)
95 | } else {
96 | return super.textContainerInset
97 | }
98 | }
99 | }
100 | }
101 |
102 |
103 | /// IMUITextSelectionMutableRect defines an annotated selection rect used by the system to
104 | /// offer rich text interaction experience. It also serves as an abstract class
105 | /// provided to be subclassed when adopting UITextInput
106 | private class IMUITextSelectionMutableRect: UITextSelectionRect {
107 |
108 | let offset: CGPoint
109 |
110 | let textSelectionRect: UITextSelectionRect
111 |
112 | init(_ textSelectionRect: UITextSelectionRect, offset: CGPoint) {
113 | self.textSelectionRect = textSelectionRect
114 | self.offset = offset
115 | super.init()
116 | }
117 |
118 | override var rect: CGRect {
119 | CGRect(x: textSelectionRect.rect.minX + offset.x,
120 | y: textSelectionRect.rect.minY + offset.y,
121 | width: textSelectionRect.rect.width,
122 | height: textSelectionRect.rect.height)
123 | }
124 |
125 | override var writingDirection: NSWritingDirection {
126 | textSelectionRect.writingDirection
127 | }
128 |
129 | /// Returns YES if the rect contains the start of the selection.
130 | override var containsStart: Bool {
131 | textSelectionRect.containsStart
132 | }
133 |
134 | /// Returns YES if the rect contains the end of the selection.
135 | override var containsEnd: Bool {
136 | textSelectionRect.containsEnd
137 | }
138 |
139 | /// Returns YES if the rect is for vertically oriented text.
140 | override var isVertical: Bool {
141 | textSelectionRect.isVertical
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/IMITextView/IMITextView.h:
--------------------------------------------------------------------------------
1 | //
2 | // IMITextView.h
3 | // IMITextView
4 | //
5 | // Created by immortal on 2021/4/29
6 | // Copyright (c) 2021 immortal. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for IMTextView.
12 | FOUNDATION_EXPORT double IMTextViewVersionNumber;
13 |
14 | //! Project version string for IMTextView.
15 | FOUNDATION_EXPORT const unsigned char IMTextViewVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/IMITextView/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Images/demon001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/immortal-it/IMITextView/063c02d414a501b6d28c9e43d13187eab4de4788/Images/demon001.png
--------------------------------------------------------------------------------
/Images/demon002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/immortal-it/IMITextView/063c02d414a501b6d28c9e43d13187eab4de4788/Images/demon002.png
--------------------------------------------------------------------------------
/Images/demon003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/immortal-it/IMITextView/063c02d414a501b6d28c9e43d13187eab4de4788/Images/demon003.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Immortal. All rights reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | //
3 | // Package.swift
4 | //
5 | // Created by immortal on 2021/4/29.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 | import PackageDescription
26 |
27 | let package = Package(name: "IMITextView",
28 | platforms: [.iOS(.v11)],
29 | products: [.library(name: "IMITextView", targets: ["IMITextView"])],
30 | targets: [.target(name: "IMITextView", path: "IMITextView")],
31 | swiftLanguageVersions: [.v5])
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IMITextView
2 |
3 | 
4 | 
5 | 
6 | [](https://cocoapods.org)
7 | [](https://github.com/Carthage/Carthage)
8 |
9 | `IMITextView` provides background effects of textView like Instagram in iOS.
10 |
11 |
12 |
13 |
14 |
15 | ## Requirements
16 |
17 | - iOS 11.0+
18 | - Xcode 12+
19 | - Swift 5.0+
20 |
21 | ## Installation
22 |
23 | ### From CocoaPods
24 |
25 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects, which automates and simplifies the process of using 3rd-party libraries like `IMITextView` in your projects. First, add the following line to your [Podfile](http://guides.cocoapods.org/using/using-cocoapods.html):
26 |
27 | ```ruby
28 | pod 'IMITextView'
29 | ```
30 |
31 | If you want to use the latest features of `IMITextView` use normal external source dependencies.
32 |
33 | ```ruby
34 | pod 'IMITextView', :git => 'https://github.com/immortal-it/IMITextView.git'
35 | ```
36 |
37 | This pulls from the `main` branch directly.
38 |
39 | Second, install `IMITextView` into your project:
40 |
41 | ```ruby
42 | pod install
43 | ```
44 |
45 | ### Carthage
46 |
47 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate IMITextView into your Xcode project using Carthage, specify it in your `Cartfile`:
48 |
49 | ```ogdl
50 | github "immortal-it/IMITextView" ~> 0.0.1
51 | ```
52 |
53 | ### Swift Package Manager
54 |
55 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but IMITextView does support its use on supported platforms.
56 |
57 | Once you have your Swift package set up, adding IMITextView as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
58 |
59 | ```swift
60 | dependencies: [
61 | .package(url: "https://github.com/immortal-it/IMITextView", .upToNextMajor(from: "0.0.1"))
62 | ]
63 | ```
64 |
65 | ### Manually
66 |
67 | * Drag the `immortal-it/IMITextView` folder into your project.
68 |
69 | ## Usage
70 |
71 | (see sample Xcode project in `Demon`)
72 |
73 | - #### Line Background
74 | ```swift
75 | let textView = IMITextView()
76 | textView.configuration.lineBackgroundOptions = .fill
77 | ```
78 | - #### Line Background Boder
79 | ```swift
80 | let textView = IMITextView()
81 | textView.configuration.lineBackgroundOptions = .boder
82 | ```
83 |
84 | - #### Stroke Outer
85 | ```swift
86 | let textView = IMITextView()
87 | textView.configuration.isStrokeOuter = true
88 | textView.configuration.strokeWidth = 10.0
89 | textView.configuration.strokeColor = .red
90 | ```
91 |
92 | ## Customization
93 |
94 | `IMITextView` can be customized via the `Configuration`
95 |
96 | ## License
97 |
98 | `IMITextView` is distributed under the terms and conditions of the [MIT license](https://github.com/immortal-it/IMITextView/LICENSE).
99 |
--------------------------------------------------------------------------------