├── 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 | ![Pod Version](https://img.shields.io/cocoapods/v/IMITextView.svg?style=flat) 4 | ![Pod Platform](https://img.shields.io/cocoapods/p/IMITextView.svg?style=flat) 5 | ![Pod License](https://img.shields.io/cocoapods/l/IMITextView.svg?style=flat) 6 | [![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-green.svg?style=flat)](https://cocoapods.org) 7 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](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 | --------------------------------------------------------------------------------