├── .swift-version ├── DEMO ├── DWCoreTextLabel.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── DWCoreTextLabel.xcscmblueprint │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── zhangdingwen.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ ├── Wicky.xcuserdatad │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ │ ├── DWCoreTextLabel.xcscheme │ │ │ └── xcschememanagement.plist │ │ ├── xbm.xcuserdatad │ │ └── xcschemes │ │ │ ├── DWCoreTextLabel.xcscheme │ │ │ └── xcschememanagement.plist │ │ └── zhangdingwen.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── DWCoreTextLabel │ ├── 2.jpg │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── ViewController.h │ ├── ViewController.m │ ├── main.m │ └── oldDriver.png ├── DWCoreTextLabelTests │ ├── DWCoreTextLabelTests.m │ └── Info.plist └── DWCoreTextLabelUITests │ ├── DWCoreTextLabelUITests.m │ └── Info.plist ├── DWCoreTextLabel.podspec ├── DWCoreTextLabel ├── DWAsyncLayer.h ├── DWAsyncLayer.m ├── DWCoreTextCommon.h ├── DWCoreTextCommon.m ├── DWCoreTextLabel.h ├── DWCoreTextLabel.m ├── DWCoreTextLabelCalculator.h ├── DWCoreTextLabelCalculator.m ├── DWCoreTextLayout.h ├── DWCoreTextLayout.m ├── DWCoreTextSelectionView.h ├── DWCoreTextSelectionView.m ├── DWWebImage.h └── DWWebImage.m ├── LICENSE ├── README.md └── 动画展示.gif /.swift-version: -------------------------------------------------------------------------------- 1 | 2.3 2 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B2CFE861E0E632D00D24B49 /* oldDriver.png in Resources */ = {isa = PBXBuildFile; fileRef = 9B2CFE851E0E632D00D24B49 /* oldDriver.png */; }; 11 | 9B786E3B1DF5D3BF00D57DAD /* 2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 9B786E3A1DF5D3BF00D57DAD /* 2.jpg */; }; 12 | 9B7BF98D1F20972300FCBDDC /* DWCoreTextLabelCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B7BF98C1F20972300FCBDDC /* DWCoreTextLabelCalculator.m */; }; 13 | 9BA02F3B1DF44C2000AB9565 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA02F3A1DF44C2000AB9565 /* main.m */; }; 14 | 9BA02F3E1DF44C2000AB9565 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA02F3D1DF44C2000AB9565 /* AppDelegate.m */; }; 15 | 9BA02F411DF44C2000AB9565 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA02F401DF44C2000AB9565 /* ViewController.m */; }; 16 | 9BA02F441DF44C2000AB9565 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA02F421DF44C2000AB9565 /* Main.storyboard */; }; 17 | 9BA02F461DF44C2000AB9565 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9BA02F451DF44C2000AB9565 /* Assets.xcassets */; }; 18 | 9BA02F491DF44C2000AB9565 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA02F471DF44C2000AB9565 /* LaunchScreen.storyboard */; }; 19 | 9BA02F541DF44C2000AB9565 /* DWCoreTextLabelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA02F531DF44C2000AB9565 /* DWCoreTextLabelTests.m */; }; 20 | 9BA02F5F1DF44C2000AB9565 /* DWCoreTextLabelUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA02F5E1DF44C2000AB9565 /* DWCoreTextLabelUITests.m */; }; 21 | 9BB485DD1F3C003100B0CDDA /* DWCoreTextLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BB485DC1F3C003100B0CDDA /* DWCoreTextLayout.m */; }; 22 | 9BC121101F5694D50064010D /* DWCoreTextSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BC1210F1F5694D50064010D /* DWCoreTextSelectionView.m */; }; 23 | 9BC121181F586E530064010D /* DWCoreTextCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BC121171F586E530064010D /* DWCoreTextCommon.m */; }; 24 | 9BECE7601F14E3DC00AAB61B /* DWAsyncLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE75B1F14E3DC00AAB61B /* DWAsyncLayer.m */; }; 25 | 9BECE7611F14E3DC00AAB61B /* DWCoreTextLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE75D1F14E3DC00AAB61B /* DWCoreTextLabel.m */; }; 26 | 9BECE7621F14E3DC00AAB61B /* DWWebImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BECE75F1F14E3DC00AAB61B /* DWWebImage.m */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 9BA02F501DF44C2000AB9565 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 9BA02F2E1DF44C2000AB9565 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 9BA02F351DF44C2000AB9565; 35 | remoteInfo = DWCoreTextLabel; 36 | }; 37 | 9BA02F5B1DF44C2000AB9565 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 9BA02F2E1DF44C2000AB9565 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 9BA02F351DF44C2000AB9565; 42 | remoteInfo = DWCoreTextLabel; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 9B2CFE851E0E632D00D24B49 /* oldDriver.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = oldDriver.png; sourceTree = ""; }; 48 | 9B786E3A1DF5D3BF00D57DAD /* 2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 2.jpg; sourceTree = ""; }; 49 | 9B7BF98B1F20972300FCBDDC /* DWCoreTextLabelCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCoreTextLabelCalculator.h; sourceTree = ""; }; 50 | 9B7BF98C1F20972300FCBDDC /* DWCoreTextLabelCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextLabelCalculator.m; sourceTree = ""; }; 51 | 9BA02F361DF44C2000AB9565 /* DWCoreTextLabel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DWCoreTextLabel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 9BA02F3A1DF44C2000AB9565 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 53 | 9BA02F3C1DF44C2000AB9565 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 54 | 9BA02F3D1DF44C2000AB9565 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 55 | 9BA02F3F1DF44C2000AB9565 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 56 | 9BA02F401DF44C2000AB9565 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 57 | 9BA02F431DF44C2000AB9565 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 58 | 9BA02F451DF44C2000AB9565 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | 9BA02F481DF44C2000AB9565 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 60 | 9BA02F4A1DF44C2000AB9565 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 9BA02F4F1DF44C2000AB9565 /* DWCoreTextLabelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DWCoreTextLabelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 9BA02F531DF44C2000AB9565 /* DWCoreTextLabelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextLabelTests.m; sourceTree = ""; }; 63 | 9BA02F551DF44C2000AB9565 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 9BA02F5A1DF44C2000AB9565 /* DWCoreTextLabelUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DWCoreTextLabelUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 9BA02F5E1DF44C2000AB9565 /* DWCoreTextLabelUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextLabelUITests.m; sourceTree = ""; }; 66 | 9BA02F601DF44C2000AB9565 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 9BB485DB1F3C003100B0CDDA /* DWCoreTextLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCoreTextLayout.h; sourceTree = ""; }; 68 | 9BB485DC1F3C003100B0CDDA /* DWCoreTextLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextLayout.m; sourceTree = ""; }; 69 | 9BC1210E1F5694D50064010D /* DWCoreTextSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCoreTextSelectionView.h; sourceTree = ""; }; 70 | 9BC1210F1F5694D50064010D /* DWCoreTextSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextSelectionView.m; sourceTree = ""; }; 71 | 9BC121161F586E520064010D /* DWCoreTextCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCoreTextCommon.h; sourceTree = ""; }; 72 | 9BC121171F586E530064010D /* DWCoreTextCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextCommon.m; sourceTree = ""; }; 73 | 9BECE75A1F14E3DC00AAB61B /* DWAsyncLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWAsyncLayer.h; sourceTree = ""; }; 74 | 9BECE75B1F14E3DC00AAB61B /* DWAsyncLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWAsyncLayer.m; sourceTree = ""; }; 75 | 9BECE75C1F14E3DC00AAB61B /* DWCoreTextLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWCoreTextLabel.h; sourceTree = ""; }; 76 | 9BECE75D1F14E3DC00AAB61B /* DWCoreTextLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWCoreTextLabel.m; sourceTree = ""; }; 77 | 9BECE75E1F14E3DC00AAB61B /* DWWebImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWWebImage.h; sourceTree = ""; }; 78 | 9BECE75F1F14E3DC00AAB61B /* DWWebImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DWWebImage.m; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 9BA02F331DF44C2000AB9565 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | 9BA02F4C1DF44C2000AB9565 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | 9BA02F571DF44C2000AB9565 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | 9BA02F2D1DF44C2000AB9565 = { 107 | isa = PBXGroup; 108 | children = ( 109 | 9BA02F381DF44C2000AB9565 /* DWCoreTextLabel */, 110 | 9BA02F521DF44C2000AB9565 /* DWCoreTextLabelTests */, 111 | 9BA02F5D1DF44C2000AB9565 /* DWCoreTextLabelUITests */, 112 | 9BA02F371DF44C2000AB9565 /* Products */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 9BA02F371DF44C2000AB9565 /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 9BA02F361DF44C2000AB9565 /* DWCoreTextLabel.app */, 120 | 9BA02F4F1DF44C2000AB9565 /* DWCoreTextLabelTests.xctest */, 121 | 9BA02F5A1DF44C2000AB9565 /* DWCoreTextLabelUITests.xctest */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | 9BA02F381DF44C2000AB9565 /* DWCoreTextLabel */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 9BECE7591F14E3DC00AAB61B /* DWCoreTextLabel */, 130 | 9BA02F3C1DF44C2000AB9565 /* AppDelegate.h */, 131 | 9BA02F3D1DF44C2000AB9565 /* AppDelegate.m */, 132 | 9B2CFE851E0E632D00D24B49 /* oldDriver.png */, 133 | 9B786E3A1DF5D3BF00D57DAD /* 2.jpg */, 134 | 9BA02F3F1DF44C2000AB9565 /* ViewController.h */, 135 | 9BA02F401DF44C2000AB9565 /* ViewController.m */, 136 | 9BA02F421DF44C2000AB9565 /* Main.storyboard */, 137 | 9BA02F451DF44C2000AB9565 /* Assets.xcassets */, 138 | 9BA02F471DF44C2000AB9565 /* LaunchScreen.storyboard */, 139 | 9BA02F4A1DF44C2000AB9565 /* Info.plist */, 140 | 9BA02F391DF44C2000AB9565 /* Supporting Files */, 141 | ); 142 | path = DWCoreTextLabel; 143 | sourceTree = ""; 144 | }; 145 | 9BA02F391DF44C2000AB9565 /* Supporting Files */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 9BA02F3A1DF44C2000AB9565 /* main.m */, 149 | ); 150 | name = "Supporting Files"; 151 | sourceTree = ""; 152 | }; 153 | 9BA02F521DF44C2000AB9565 /* DWCoreTextLabelTests */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 9BA02F531DF44C2000AB9565 /* DWCoreTextLabelTests.m */, 157 | 9BA02F551DF44C2000AB9565 /* Info.plist */, 158 | ); 159 | path = DWCoreTextLabelTests; 160 | sourceTree = ""; 161 | }; 162 | 9BA02F5D1DF44C2000AB9565 /* DWCoreTextLabelUITests */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 9BA02F5E1DF44C2000AB9565 /* DWCoreTextLabelUITests.m */, 166 | 9BA02F601DF44C2000AB9565 /* Info.plist */, 167 | ); 168 | path = DWCoreTextLabelUITests; 169 | sourceTree = ""; 170 | }; 171 | 9BECE7591F14E3DC00AAB61B /* DWCoreTextLabel */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 9BECE75A1F14E3DC00AAB61B /* DWAsyncLayer.h */, 175 | 9BECE75B1F14E3DC00AAB61B /* DWAsyncLayer.m */, 176 | 9BECE75C1F14E3DC00AAB61B /* DWCoreTextLabel.h */, 177 | 9BECE75D1F14E3DC00AAB61B /* DWCoreTextLabel.m */, 178 | 9BC1210E1F5694D50064010D /* DWCoreTextSelectionView.h */, 179 | 9BC1210F1F5694D50064010D /* DWCoreTextSelectionView.m */, 180 | 9BB485DB1F3C003100B0CDDA /* DWCoreTextLayout.h */, 181 | 9BB485DC1F3C003100B0CDDA /* DWCoreTextLayout.m */, 182 | 9BC121161F586E520064010D /* DWCoreTextCommon.h */, 183 | 9BC121171F586E530064010D /* DWCoreTextCommon.m */, 184 | 9B7BF98B1F20972300FCBDDC /* DWCoreTextLabelCalculator.h */, 185 | 9B7BF98C1F20972300FCBDDC /* DWCoreTextLabelCalculator.m */, 186 | 9BECE75E1F14E3DC00AAB61B /* DWWebImage.h */, 187 | 9BECE75F1F14E3DC00AAB61B /* DWWebImage.m */, 188 | ); 189 | name = DWCoreTextLabel; 190 | path = ../../DWCoreTextLabel; 191 | sourceTree = ""; 192 | }; 193 | /* End PBXGroup section */ 194 | 195 | /* Begin PBXNativeTarget section */ 196 | 9BA02F351DF44C2000AB9565 /* DWCoreTextLabel */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = 9BA02F631DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabel" */; 199 | buildPhases = ( 200 | 9BA02F321DF44C2000AB9565 /* Sources */, 201 | 9BA02F331DF44C2000AB9565 /* Frameworks */, 202 | 9BA02F341DF44C2000AB9565 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | ); 208 | name = DWCoreTextLabel; 209 | productName = DWCoreTextLabel; 210 | productReference = 9BA02F361DF44C2000AB9565 /* DWCoreTextLabel.app */; 211 | productType = "com.apple.product-type.application"; 212 | }; 213 | 9BA02F4E1DF44C2000AB9565 /* DWCoreTextLabelTests */ = { 214 | isa = PBXNativeTarget; 215 | buildConfigurationList = 9BA02F661DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabelTests" */; 216 | buildPhases = ( 217 | 9BA02F4B1DF44C2000AB9565 /* Sources */, 218 | 9BA02F4C1DF44C2000AB9565 /* Frameworks */, 219 | 9BA02F4D1DF44C2000AB9565 /* Resources */, 220 | ); 221 | buildRules = ( 222 | ); 223 | dependencies = ( 224 | 9BA02F511DF44C2000AB9565 /* PBXTargetDependency */, 225 | ); 226 | name = DWCoreTextLabelTests; 227 | productName = DWCoreTextLabelTests; 228 | productReference = 9BA02F4F1DF44C2000AB9565 /* DWCoreTextLabelTests.xctest */; 229 | productType = "com.apple.product-type.bundle.unit-test"; 230 | }; 231 | 9BA02F591DF44C2000AB9565 /* DWCoreTextLabelUITests */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 9BA02F691DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabelUITests" */; 234 | buildPhases = ( 235 | 9BA02F561DF44C2000AB9565 /* Sources */, 236 | 9BA02F571DF44C2000AB9565 /* Frameworks */, 237 | 9BA02F581DF44C2000AB9565 /* Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | 9BA02F5C1DF44C2000AB9565 /* PBXTargetDependency */, 243 | ); 244 | name = DWCoreTextLabelUITests; 245 | productName = DWCoreTextLabelUITests; 246 | productReference = 9BA02F5A1DF44C2000AB9565 /* DWCoreTextLabelUITests.xctest */; 247 | productType = "com.apple.product-type.bundle.ui-testing"; 248 | }; 249 | /* End PBXNativeTarget section */ 250 | 251 | /* Begin PBXProject section */ 252 | 9BA02F2E1DF44C2000AB9565 /* Project object */ = { 253 | isa = PBXProject; 254 | attributes = { 255 | LastUpgradeCheck = 0800; 256 | ORGANIZATIONNAME = Wicky; 257 | TargetAttributes = { 258 | 9BA02F351DF44C2000AB9565 = { 259 | CreatedOnToolsVersion = 8.0; 260 | DevelopmentTeam = Z26Y87B8NH; 261 | ProvisioningStyle = Automatic; 262 | }; 263 | 9BA02F4E1DF44C2000AB9565 = { 264 | CreatedOnToolsVersion = 8.0; 265 | ProvisioningStyle = Automatic; 266 | TestTargetID = 9BA02F351DF44C2000AB9565; 267 | }; 268 | 9BA02F591DF44C2000AB9565 = { 269 | CreatedOnToolsVersion = 8.0; 270 | ProvisioningStyle = Automatic; 271 | TestTargetID = 9BA02F351DF44C2000AB9565; 272 | }; 273 | }; 274 | }; 275 | buildConfigurationList = 9BA02F311DF44C2000AB9565 /* Build configuration list for PBXProject "DWCoreTextLabel" */; 276 | compatibilityVersion = "Xcode 3.2"; 277 | developmentRegion = English; 278 | hasScannedForEncodings = 0; 279 | knownRegions = ( 280 | en, 281 | Base, 282 | ); 283 | mainGroup = 9BA02F2D1DF44C2000AB9565; 284 | productRefGroup = 9BA02F371DF44C2000AB9565 /* Products */; 285 | projectDirPath = ""; 286 | projectRoot = ""; 287 | targets = ( 288 | 9BA02F351DF44C2000AB9565 /* DWCoreTextLabel */, 289 | 9BA02F4E1DF44C2000AB9565 /* DWCoreTextLabelTests */, 290 | 9BA02F591DF44C2000AB9565 /* DWCoreTextLabelUITests */, 291 | ); 292 | }; 293 | /* End PBXProject section */ 294 | 295 | /* Begin PBXResourcesBuildPhase section */ 296 | 9BA02F341DF44C2000AB9565 /* Resources */ = { 297 | isa = PBXResourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | 9BA02F491DF44C2000AB9565 /* LaunchScreen.storyboard in Resources */, 301 | 9BA02F461DF44C2000AB9565 /* Assets.xcassets in Resources */, 302 | 9B786E3B1DF5D3BF00D57DAD /* 2.jpg in Resources */, 303 | 9BA02F441DF44C2000AB9565 /* Main.storyboard in Resources */, 304 | 9B2CFE861E0E632D00D24B49 /* oldDriver.png in Resources */, 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | 9BA02F4D1DF44C2000AB9565 /* Resources */ = { 309 | isa = PBXResourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | 9BA02F581DF44C2000AB9565 /* Resources */ = { 316 | isa = PBXResourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | ); 320 | runOnlyForDeploymentPostprocessing = 0; 321 | }; 322 | /* End PBXResourcesBuildPhase section */ 323 | 324 | /* Begin PBXSourcesBuildPhase section */ 325 | 9BA02F321DF44C2000AB9565 /* Sources */ = { 326 | isa = PBXSourcesBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | 9BECE7611F14E3DC00AAB61B /* DWCoreTextLabel.m in Sources */, 330 | 9BA02F411DF44C2000AB9565 /* ViewController.m in Sources */, 331 | 9BA02F3E1DF44C2000AB9565 /* AppDelegate.m in Sources */, 332 | 9BC121101F5694D50064010D /* DWCoreTextSelectionView.m in Sources */, 333 | 9B7BF98D1F20972300FCBDDC /* DWCoreTextLabelCalculator.m in Sources */, 334 | 9BB485DD1F3C003100B0CDDA /* DWCoreTextLayout.m in Sources */, 335 | 9BA02F3B1DF44C2000AB9565 /* main.m in Sources */, 336 | 9BC121181F586E530064010D /* DWCoreTextCommon.m in Sources */, 337 | 9BECE7601F14E3DC00AAB61B /* DWAsyncLayer.m in Sources */, 338 | 9BECE7621F14E3DC00AAB61B /* DWWebImage.m in Sources */, 339 | ); 340 | runOnlyForDeploymentPostprocessing = 0; 341 | }; 342 | 9BA02F4B1DF44C2000AB9565 /* Sources */ = { 343 | isa = PBXSourcesBuildPhase; 344 | buildActionMask = 2147483647; 345 | files = ( 346 | 9BA02F541DF44C2000AB9565 /* DWCoreTextLabelTests.m in Sources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | 9BA02F561DF44C2000AB9565 /* Sources */ = { 351 | isa = PBXSourcesBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | 9BA02F5F1DF44C2000AB9565 /* DWCoreTextLabelUITests.m in Sources */, 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | /* End PBXSourcesBuildPhase section */ 359 | 360 | /* Begin PBXTargetDependency section */ 361 | 9BA02F511DF44C2000AB9565 /* PBXTargetDependency */ = { 362 | isa = PBXTargetDependency; 363 | target = 9BA02F351DF44C2000AB9565 /* DWCoreTextLabel */; 364 | targetProxy = 9BA02F501DF44C2000AB9565 /* PBXContainerItemProxy */; 365 | }; 366 | 9BA02F5C1DF44C2000AB9565 /* PBXTargetDependency */ = { 367 | isa = PBXTargetDependency; 368 | target = 9BA02F351DF44C2000AB9565 /* DWCoreTextLabel */; 369 | targetProxy = 9BA02F5B1DF44C2000AB9565 /* PBXContainerItemProxy */; 370 | }; 371 | /* End PBXTargetDependency section */ 372 | 373 | /* Begin PBXVariantGroup section */ 374 | 9BA02F421DF44C2000AB9565 /* Main.storyboard */ = { 375 | isa = PBXVariantGroup; 376 | children = ( 377 | 9BA02F431DF44C2000AB9565 /* Base */, 378 | ); 379 | name = Main.storyboard; 380 | sourceTree = ""; 381 | }; 382 | 9BA02F471DF44C2000AB9565 /* LaunchScreen.storyboard */ = { 383 | isa = PBXVariantGroup; 384 | children = ( 385 | 9BA02F481DF44C2000AB9565 /* Base */, 386 | ); 387 | name = LaunchScreen.storyboard; 388 | sourceTree = ""; 389 | }; 390 | /* End PBXVariantGroup section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | 9BA02F611DF44C2000AB9565 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_NONNULL = YES; 398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 399 | CLANG_CXX_LIBRARY = "libc++"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 405 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INFINITE_RECURSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 411 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 412 | CLANG_WARN_UNREACHABLE_CODE = YES; 413 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 414 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 415 | COPY_PHASE_STRIP = NO; 416 | DEBUG_INFORMATION_FORMAT = dwarf; 417 | ENABLE_STRICT_OBJC_MSGSEND = YES; 418 | ENABLE_TESTABILITY = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu99; 420 | GCC_DYNAMIC_NO_PIC = NO; 421 | GCC_NO_COMMON_BLOCKS = YES; 422 | GCC_OPTIMIZATION_LEVEL = 0; 423 | GCC_PREPROCESSOR_DEFINITIONS = ( 424 | "DEBUG=1", 425 | "$(inherited)", 426 | ); 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 434 | MTL_ENABLE_DEBUG_INFO = YES; 435 | ONLY_ACTIVE_ARCH = YES; 436 | SDKROOT = iphoneos; 437 | TARGETED_DEVICE_FAMILY = "1,2"; 438 | }; 439 | name = Debug; 440 | }; 441 | 9BA02F621DF44C2000AB9565 /* Release */ = { 442 | isa = XCBuildConfiguration; 443 | buildSettings = { 444 | ALWAYS_SEARCH_USER_PATHS = NO; 445 | CLANG_ANALYZER_NONNULL = YES; 446 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 447 | CLANG_CXX_LIBRARY = "libc++"; 448 | CLANG_ENABLE_MODULES = YES; 449 | CLANG_ENABLE_OBJC_ARC = YES; 450 | CLANG_WARN_BOOL_CONVERSION = YES; 451 | CLANG_WARN_CONSTANT_CONVERSION = YES; 452 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 453 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 454 | CLANG_WARN_EMPTY_BODY = YES; 455 | CLANG_WARN_ENUM_CONVERSION = YES; 456 | CLANG_WARN_INFINITE_RECURSION = YES; 457 | CLANG_WARN_INT_CONVERSION = YES; 458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 459 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 460 | CLANG_WARN_UNREACHABLE_CODE = YES; 461 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 462 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 463 | COPY_PHASE_STRIP = NO; 464 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 465 | ENABLE_NS_ASSERTIONS = NO; 466 | ENABLE_STRICT_OBJC_MSGSEND = YES; 467 | GCC_C_LANGUAGE_STANDARD = gnu99; 468 | GCC_NO_COMMON_BLOCKS = YES; 469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 471 | GCC_WARN_UNDECLARED_SELECTOR = YES; 472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 473 | GCC_WARN_UNUSED_FUNCTION = YES; 474 | GCC_WARN_UNUSED_VARIABLE = YES; 475 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 476 | MTL_ENABLE_DEBUG_INFO = NO; 477 | SDKROOT = iphoneos; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | VALIDATE_PRODUCT = YES; 480 | }; 481 | name = Release; 482 | }; 483 | 9BA02F641DF44C2000AB9565 /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 487 | DEVELOPMENT_TEAM = Z26Y87B8NH; 488 | INFOPLIST_FILE = DWCoreTextLabel/Info.plist; 489 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 490 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabel; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | }; 493 | name = Debug; 494 | }; 495 | 9BA02F651DF44C2000AB9565 /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | buildSettings = { 498 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 499 | DEVELOPMENT_TEAM = Z26Y87B8NH; 500 | INFOPLIST_FILE = DWCoreTextLabel/Info.plist; 501 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 502 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabel; 503 | PRODUCT_NAME = "$(TARGET_NAME)"; 504 | }; 505 | name = Release; 506 | }; 507 | 9BA02F671DF44C2000AB9565 /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | BUNDLE_LOADER = "$(TEST_HOST)"; 511 | INFOPLIST_FILE = DWCoreTextLabelTests/Info.plist; 512 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 513 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabelTests; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DWCoreTextLabel.app/DWCoreTextLabel"; 516 | }; 517 | name = Debug; 518 | }; 519 | 9BA02F681DF44C2000AB9565 /* Release */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | BUNDLE_LOADER = "$(TEST_HOST)"; 523 | INFOPLIST_FILE = DWCoreTextLabelTests/Info.plist; 524 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 525 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabelTests; 526 | PRODUCT_NAME = "$(TARGET_NAME)"; 527 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DWCoreTextLabel.app/DWCoreTextLabel"; 528 | }; 529 | name = Release; 530 | }; 531 | 9BA02F6A1DF44C2000AB9565 /* Debug */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | INFOPLIST_FILE = DWCoreTextLabelUITests/Info.plist; 535 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 536 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabelUITests; 537 | PRODUCT_NAME = "$(TARGET_NAME)"; 538 | TEST_TARGET_NAME = DWCoreTextLabel; 539 | }; 540 | name = Debug; 541 | }; 542 | 9BA02F6B1DF44C2000AB9565 /* Release */ = { 543 | isa = XCBuildConfiguration; 544 | buildSettings = { 545 | INFOPLIST_FILE = DWCoreTextLabelUITests/Info.plist; 546 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 547 | PRODUCT_BUNDLE_IDENTIFIER = com.CodeWicky.DWCoreTextLabelUITests; 548 | PRODUCT_NAME = "$(TARGET_NAME)"; 549 | TEST_TARGET_NAME = DWCoreTextLabel; 550 | }; 551 | name = Release; 552 | }; 553 | /* End XCBuildConfiguration section */ 554 | 555 | /* Begin XCConfigurationList section */ 556 | 9BA02F311DF44C2000AB9565 /* Build configuration list for PBXProject "DWCoreTextLabel" */ = { 557 | isa = XCConfigurationList; 558 | buildConfigurations = ( 559 | 9BA02F611DF44C2000AB9565 /* Debug */, 560 | 9BA02F621DF44C2000AB9565 /* Release */, 561 | ); 562 | defaultConfigurationIsVisible = 0; 563 | defaultConfigurationName = Release; 564 | }; 565 | 9BA02F631DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabel" */ = { 566 | isa = XCConfigurationList; 567 | buildConfigurations = ( 568 | 9BA02F641DF44C2000AB9565 /* Debug */, 569 | 9BA02F651DF44C2000AB9565 /* Release */, 570 | ); 571 | defaultConfigurationIsVisible = 0; 572 | defaultConfigurationName = Release; 573 | }; 574 | 9BA02F661DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabelTests" */ = { 575 | isa = XCConfigurationList; 576 | buildConfigurations = ( 577 | 9BA02F671DF44C2000AB9565 /* Debug */, 578 | 9BA02F681DF44C2000AB9565 /* Release */, 579 | ); 580 | defaultConfigurationIsVisible = 0; 581 | defaultConfigurationName = Release; 582 | }; 583 | 9BA02F691DF44C2000AB9565 /* Build configuration list for PBXNativeTarget "DWCoreTextLabelUITests" */ = { 584 | isa = XCConfigurationList; 585 | buildConfigurations = ( 586 | 9BA02F6A1DF44C2000AB9565 /* Debug */, 587 | 9BA02F6B1DF44C2000AB9565 /* Release */, 588 | ); 589 | defaultConfigurationIsVisible = 0; 590 | defaultConfigurationName = Release; 591 | }; 592 | /* End XCConfigurationList section */ 593 | }; 594 | rootObject = 9BA02F2E1DF44C2000AB9565 /* Project object */; 595 | } 596 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/project.xcworkspace/xcshareddata/DWCoreTextLabel.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "10CF82FA390C138383AE63CE6948C0193732089F", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "10CF82FA390C138383AE63CE6948C0193732089F" : 9223372036854775807, 8 | "19A8EA9119395F091A7FCBD544D1C179ADF2D240" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "80A7E2C1-4290-4DBA-A1F9-81F03446ED8F", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "10CF82FA390C138383AE63CE6948C0193732089F" : "DWCoreTextLabel\/", 13 | "19A8EA9119395F091A7FCBD544D1C179ADF2D240" : "%E4%B8%80%E4%BA%9B%E5%B7%A5%E5%85%B7%E7%B1%BB\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "DWCoreTextLabel", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "DEMO\/DWCoreTextLabel.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CodeWicky\/DWCoreTextLabel.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "10CF82FA390C138383AE63CE6948C0193732089F" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CodeWicky\/-Tools.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "19A8EA9119395F091A7FCBD544D1C179ADF2D240" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/project.xcworkspace/xcuserdata/zhangdingwen.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWicky/DWCoreTextLabel/ee68b17907c7095d4cb0294294aad2cc6d8b186e/DEMO/DWCoreTextLabel.xcodeproj/project.xcworkspace/xcuserdata/zhangdingwen.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/Wicky.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 24 | 36 | 37 | 38 | 40 | 52 | 53 | 54 | 56 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/Wicky.xcuserdatad/xcschemes/DWCoreTextLabel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 100 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/Wicky.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DWCoreTextLabel.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 9BA02F351DF44C2000AB9565 16 | 17 | primary 18 | 19 | 20 | 9BA02F4E1DF44C2000AB9565 21 | 22 | primary 23 | 24 | 25 | 9BA02F591DF44C2000AB9565 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/xbm.xcuserdatad/xcschemes/DWCoreTextLabel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/xbm.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DWCoreTextLabel.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 9BA02F351DF44C2000AB9565 16 | 17 | primary 18 | 19 | 20 | 9BA02F4E1DF44C2000AB9565 21 | 22 | primary 23 | 24 | 25 | 9BA02F591DF44C2000AB9565 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel.xcodeproj/xcuserdata/zhangdingwen.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | DWCoreTextLabel.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWicky/DWCoreTextLabel/ee68b17907c7095d4cb0294294aad2cc6d8b186e/DEMO/DWCoreTextLabel/2.jpg -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/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 | 27 | 28 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/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 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | en 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "DWCoreTextLabel.h" 11 | @interface ViewController () 12 | 13 | @property (nonatomic ,strong) DWCoreTextLabel * label; 14 | 15 | @end 16 | 17 | @implementation ViewController 18 | 19 | - (void)viewDidLoad { 20 | [super viewDidLoad]; 21 | 22 | 23 | DWCoreTextLabel * label = [[DWCoreTextLabel alloc] initWithFrame:self.view.bounds]; 24 | self.label = label; 25 | label.text = @"姓名:\t老司机\n性别:\t男\n年龄:\t18+\n现居地:\t北京\n爱好:\t女\n简历:你就想想一个逗逼程序员是什么样,老司机就是什么样。嗯,如果不了解程序员这个行业,你就想想逗逼什么样吧。\n\n欢迎各位女程序员前来骚扰,男程序员们申请个女号再来骚扰。\n简书地址:http://www.jianshu.com/users/a56ec10f6603/latest_articles\nGitHub:https://github.com/CodeWicky\nDWCoreTextLabel简介:\nDWCoreTextLabel最大的特点是这是一个支持图片环绕文本、添加文字图片点击事件的一个控件,它是基于CoreText致力于让你替换系统Label的一个日常化组件。目前作者正在努力完善其他功能中~恩,这之所以写这么多字,是因为我要展示一下环绕文字的效果。"; 26 | label.backgroundColor = [UIColor colorWithRed:253 / 255.0 green:249 / 255.0 blue:218 / 255.0 alpha:1]; 27 | label.textInsets = UIEdgeInsetsMake(10, 10, 10, 10); 28 | label.textColor = [UIColor blueColor]; 29 | // label.numberOfLines = 2; 30 | [self.view addSubview:label]; 31 | label.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(10, 10, 120, 120)]].mutableCopy; 32 | 33 | CGFloat width = [UIScreen mainScreen].bounds.size.width; 34 | CGFloat padding = 10; 35 | width -= (padding * 2 + label.textInsets.left + label.textInsets.right); 36 | 37 | [label dw_InsertImage:[UIImage imageNamed:@"2.jpg"] withImageID:@"URL" size:CGSizeMake(width, width * 9 / 16.0) padding:padding descent:0 atLocation:91 target:self selector:@selector(clickPic)]; 38 | [label dw_DrawImage:[UIImage imageNamed:@"oldDriver"] withImageID:@"URL" path:[UIBezierPath bezierPathWithOvalInRect:CGRectMake(10,10, 120, 120)] margin:0 drawMode:(DWTextImageDrawModeCover) target:self selector:@selector(clickHeader)]; 39 | [label dw_AddTarget:self selector:@selector(clickLink) toRange:NSMakeRange(126, 57)]; 40 | [label dw_AddTarget:self selector:@selector(clickBlog) toRange:NSMakeRange(191, 28)]; 41 | label.delegate = self; 42 | label.autoCheckLink = YES; 43 | NSDictionary * dic = @{NSForegroundColorAttributeName:[UIColor redColor]}; 44 | label.activeTextAttributes = dic; 45 | // label.textVerticalAlignment = DWTextVerticalAlignmentBottom; 46 | NSDictionary * dic2 = @{NSForegroundColorAttributeName:[UIColor greenColor]}; 47 | label.activeTextHighlightAttributes = dic2; 48 | UIBezierPath * path = [UIBezierPath bezierPath]; 49 | [path moveToPoint:CGPointMake(self.view.center.x, 575)]; 50 | [path addLineToPoint:CGPointMake(self.view.center.x - 50, 625)]; 51 | [path addLineToPoint:CGPointMake(self.view.center.x, 675)]; 52 | [path addLineToPoint:CGPointMake(self.view.center.x + 50, 625)]; 53 | [path closePath]; 54 | [label dw_DrawImageWithUrl:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1486655338141&di=f457d61d52460455bd37430fe77b5cf4&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fpic%2Fitem%2F4c8e31a03bb6ec2cf31fe7b4.jpg" withImageID:@"URL" placeHolder:nil path:path margin:0 drawMode:(DWTextImageDrawModeSurround) target:nil selector:nil]; 55 | 56 | } 57 | 58 | -(void)coreTextLabel:(DWCoreTextLabel *)label didSelectLink:(NSString *)link range:(NSRange)range linkType:(DWLinkType)linkType 59 | { 60 | NSLog(@"%@ == %@ == %ld",link,NSStringFromRange(range),linkType); 61 | } 62 | 63 | -(void)clickHeader 64 | { 65 | [[[UIAlertView alloc] initWithTitle:nil message:@"你点我头像嘎哈!" delegate:nil cancelButtonTitle:@"我错了" otherButtonTitles:nil] show]; 66 | } 67 | 68 | -(void)clickPic 69 | { 70 | [[[UIAlertView alloc] initWithTitle:nil message:@"你点击了图片!" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil] show]; 71 | } 72 | 73 | -(void)clickLink 74 | { 75 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://www.jianshu.com/users/a56ec10f6603/latest_articles"]]; 76 | } 77 | 78 | -(void)clickBlog 79 | { 80 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/CodeWicky"]]; 81 | } 82 | - (void)didReceiveMemoryWarning { 83 | [super didReceiveMemoryWarning]; 84 | // Dispose of any resources that can be recreated. 85 | } 86 | 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabel/oldDriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWicky/DWCoreTextLabel/ee68b17907c7095d4cb0294294aad2cc6d8b186e/DEMO/DWCoreTextLabel/oldDriver.png -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabelTests/DWCoreTextLabelTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLabelTests.m 3 | // DWCoreTextLabelTests 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DWCoreTextLabelTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation DWCoreTextLabelTests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | - (void)tearDown { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample { 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | - (void)testPerformanceExample { 33 | // This is an example of a performance test case. 34 | [self measureBlock:^{ 35 | // Put the code you want to measure the time of here. 36 | }]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabelTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabelUITests/DWCoreTextLabelUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLabelUITests.m 3 | // DWCoreTextLabelUITests 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DWCoreTextLabelUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation DWCoreTextLabelUITests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | 22 | // In UI tests it is usually best to stop immediately when a failure occurs. 23 | self.continueAfterFailure = NO; 24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 25 | [[[XCUIApplication alloc] init] launch]; 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | - (void)tearDown { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | [super tearDown]; 33 | } 34 | 35 | - (void)testExample { 36 | // Use recording to get started writing UI tests. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /DEMO/DWCoreTextLabelUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DWCoreTextLabel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DWCoreTextLabel' 3 | s.version = '1.2.4' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'It is a Label based on coreText,help you to layout of the collocation of illustration and character.基于coreText的Label控件,帮助你做图文混排。' 6 | s.homepage = 'https://github.com/CodeWicky/DWCoreTextLabel' 7 | s.authors = { 'codeWicky' => 'codewicky@163.com' } 8 | s.social_media_url = 'http://www.jianshu.com/u/a56ec10f6603' 9 | s.source = { :git => 'https://github.com/CodeWicky/DWCoreTextLabel.git', :tag => s.version.to_s } 10 | s.requires_arc = true 11 | s.ios.deployment_target = '7.0' 12 | s.source_files = 'DWCoreTextLabel/*.{h,m}' 13 | s.frameworks = 'UIKit' 14 | end 15 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWAsyncLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWAsyncLayer.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/2/9. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | /** 10 | 图层异步绘制类 11 | 12 | 提供图层的异步绘制,线程安全。 13 | 14 | version 1.0.0 15 | 提供异步绘制 16 | */ 17 | 18 | #import 19 | #import 20 | 21 | @interface DWAsyncLayer : CALayer 22 | 23 | @property (nonatomic ,copy) void (^displayBlock)(CGContextRef context,BOOL(^isCanceled)(void)); 24 | 25 | @property (nonatomic ,assign) BOOL displaysAsynchronously; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWAsyncLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWAsyncLayer.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/2/9. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWAsyncLayer.h" 10 | #import 11 | 12 | static dispatch_queue_t DWCoreTextLabelLayerGetDisplayQueue() { 13 | #define MAX_QUEUE_COUNT 16 14 | static int queueCount; 15 | static dispatch_queue_t queues[MAX_QUEUE_COUNT]; 16 | static dispatch_once_t onceToken; 17 | static int32_t counter = 0; 18 | dispatch_once(&onceToken, ^{ 19 | queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount; 20 | queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount; 21 | if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { 22 | for (NSUInteger i = 0; i < queueCount; i++) { 23 | dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); 24 | queues[i] = dispatch_queue_create("com.codeWicky.DWCoreTextLabel.render", attr); 25 | } 26 | } else { 27 | for (NSUInteger i = 0; i < queueCount; i++) { 28 | queues[i] = dispatch_queue_create("com.codeWicky.DWCoreTextLabel.render", DISPATCH_QUEUE_SERIAL); 29 | dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); 30 | } 31 | } 32 | }); 33 | #pragma clang diagnostic push 34 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 35 | uint32_t cur = (uint32_t)OSAtomicIncrement32(&counter); 36 | #pragma clang diagnostic pop 37 | return queues[(cur) % queueCount]; 38 | #undef MAX_QUEUE_COUNT 39 | } 40 | 41 | static dispatch_queue_t DWCoreTextLabelLayerGetReleaseQueue() { 42 | return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 43 | } 44 | 45 | @interface DWAsyncLayer () 46 | 47 | @property (atomic, readonly) int32_t signal; 48 | 49 | @end 50 | 51 | @implementation DWAsyncLayer 52 | 53 | -(instancetype)init 54 | { 55 | self = [super init]; 56 | if (self) { 57 | _signal = 0; 58 | _displaysAsynchronously = YES; 59 | } 60 | return self; 61 | } 62 | 63 | -(void)signalIncrease 64 | { 65 | #pragma clang diagnostic push 66 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 67 | OSAtomicIncrement32(&_signal); 68 | #pragma clang diagnostic pop 69 | } 70 | 71 | -(void)setNeedsDisplay 72 | { 73 | [self cancelPreviousDisplayCalculate]; 74 | [super setNeedsDisplay]; 75 | } 76 | 77 | -(void)cancelPreviousDisplayCalculate 78 | { 79 | [self signalIncrease]; 80 | } 81 | 82 | -(void)dealloc 83 | { 84 | [self cancelPreviousDisplayCalculate]; 85 | } 86 | 87 | -(void)display 88 | { 89 | super.contents = super.contents; 90 | [self displayAsync:self.displaysAsynchronously]; 91 | } 92 | 93 | -(void)displayAsync:(BOOL)async 94 | { 95 | if (!self.displayBlock) { 96 | self.contents = nil; 97 | return; 98 | } 99 | if (async) { 100 | int32_t signal = self.signal; 101 | BOOL (^isCancelled)(void) = ^BOOL(void) { 102 | return signal != self.signal; 103 | }; 104 | CGSize size = self.bounds.size; 105 | BOOL opaque = self.opaque; 106 | CGFloat scale = self.contentsScale; 107 | CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL; 108 | if (size.width < 1 || size.height < 1) { 109 | CGImageRef image = (__bridge_retained CGImageRef)(self.contents); 110 | self.contents = nil; 111 | if (image) { 112 | dispatch_async(DWCoreTextLabelLayerGetReleaseQueue(), ^{ 113 | CFRelease(image); 114 | }); 115 | } 116 | CGColorRelease(backgroundColor); 117 | return; 118 | } 119 | 120 | dispatch_async(DWCoreTextLabelLayerGetDisplayQueue(), ^{ 121 | if (isCancelled()) { 122 | CGColorRelease(backgroundColor); 123 | return; 124 | } 125 | UIGraphicsBeginImageContextWithOptions(size, opaque, scale); 126 | CGContextRef context = UIGraphicsGetCurrentContext(); 127 | if (opaque) { 128 | fillContextWithColor(context, backgroundColor, size); 129 | CGColorRelease(backgroundColor); 130 | } 131 | self.displayBlock(context,isCancelled); 132 | if (isCancelled()) { 133 | UIGraphicsEndImageContext(); 134 | return; 135 | } 136 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 137 | UIGraphicsEndImageContext(); 138 | if (isCancelled()) { 139 | return; 140 | } 141 | dispatch_async(dispatch_get_main_queue(), ^{ 142 | if (!isCancelled()) { 143 | self.contents = (__bridge id)(image.CGImage); 144 | } 145 | }); 146 | }); 147 | } else { 148 | [self signalIncrease]; 149 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale); 150 | CGContextRef context = UIGraphicsGetCurrentContext(); 151 | if (self.opaque) { 152 | CGSize size = self.bounds.size; 153 | size.width *= self.contentsScale; 154 | size.height *= self.contentsScale; 155 | fillContextWithColor(context, self.backgroundColor,size); 156 | } 157 | self.displayBlock(context,^{return NO;}); 158 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 159 | UIGraphicsEndImageContext(); 160 | self.contents = (__bridge id)(image.CGImage); 161 | } 162 | } 163 | 164 | static inline void fillContextWithColor(CGContextRef context,CGColorRef color,CGSize size){ 165 | CGContextSaveGState(context); { 166 | if (!color || CGColorGetAlpha(color) < 1) { 167 | CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); 168 | CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height)); 169 | CGContextFillPath(context); 170 | } 171 | if (color) { 172 | CGContextSetFillColorWithColor(context, color); 173 | CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height)); 174 | CGContextFillPath(context); 175 | } 176 | } CGContextRestoreGState(context); 177 | }; 178 | 179 | @end 180 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextCommon.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextCommon.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/9/1. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #pragma mark --- DWPpsition --- 12 | typedef struct { 13 | CGFloat baseLineY;///基线纵坐标 14 | CGFloat xCrd;///x点横坐标 15 | CGFloat height;///高度 16 | NSUInteger index;///角标 17 | } DWPosition; 18 | 19 | /** 20 | 0位置,{0,0,0} 21 | */ 22 | UIKIT_EXTERN DWPosition const DWPositionZero; 23 | 24 | /** 25 | 空位置,{MAXFLOAT,MAXFLOAT,MAXFLOAT} 26 | */ 27 | UIKIT_EXTERN DWPosition const DWPositionNull; 28 | 29 | 30 | /** 31 | 初始化位置结构体 32 | 33 | @param baseLineY 基线纵坐标 34 | @param xCrd 横坐标 35 | @param height 位置高度 36 | @return 结构体 37 | */ 38 | NS_INLINE DWPosition DWMakePosition(CGFloat baseLineY,CGFloat xCrd,CGFloat height,NSUInteger index) { 39 | return (DWPosition){baseLineY,xCrd,height,index}; 40 | } 41 | 42 | 43 | /** 44 | 获取位置基点 45 | 46 | @param p 位置 47 | @return 基点 48 | */ 49 | NS_INLINE CGPoint DWPositionGetBaseOrigin(DWPosition p) { 50 | return CGPointMake(p.xCrd, p.baseLineY); 51 | } 52 | 53 | 54 | /** 55 | 比较两位置是否相同 56 | 57 | @param p1 位置1 58 | @param p2 位置2 59 | @return 比较结果 60 | 61 | 注: 62 | baseLineY、xCrd、height相同的两个位置即相同 63 | */ 64 | NS_INLINE BOOL DWPositionEqualToPosition(DWPosition p1,DWPosition p2) { 65 | return (p1.baseLineY == p2.baseLineY) && (p1.xCrd == p2.xCrd) && (p1.height == p2.height); 66 | } 67 | 68 | 69 | /** 70 | 比较当前位置是否为空 71 | 72 | @param p 位置 73 | @return 比较结果 74 | */ 75 | BOOL DWPositionIsNull(DWPosition p); 76 | 77 | 78 | /** 79 | 比较当前位置是否为0 80 | 81 | @param p 位置 82 | @return 比较结果 83 | */ 84 | BOOL DWPositionIsZero(DWPosition p); 85 | 86 | 87 | /** 88 | 比较两位置空间相对位置(高度忽略) 89 | 90 | @param p1 位置1 91 | @param p2 位置2 92 | @return 比较结果 93 | 94 | 注: 95 | 即比较baseOrigin的相对位置,返回值规则同DWComparePoint() 96 | */ 97 | NSComparisonResult DWComparePosition(DWPosition p1,DWPosition p2); 98 | 99 | 100 | /** 101 | 比较两位置空间相对位置是否相同(高度忽略) 102 | 103 | @param p1 位置1 104 | @param p2 位置2 105 | @return 比较结果 106 | 107 | 注: 108 | 即比较baseOrigin是否相同 109 | */ 110 | BOOL DWPositionBaseOriginEqualToPosition(DWPosition p1,DWPosition p2); 111 | 112 | 113 | /** 114 | 根据位置及宽度返回尺寸 115 | 116 | @param p 位置 117 | @param width 宽度 118 | @return 尺寸 119 | */ 120 | CGRect CGRectFromPosition(DWPosition p,CGFloat width); 121 | 122 | #pragma mark --- NSRange --- 123 | /** 124 | 零范围,{0,0} 125 | */ 126 | UIKIT_EXTERN NSRange const NSRangeZero; 127 | 128 | /** 129 | 空范围,{MAXFLOAT,MAXFLOAT} 130 | */ 131 | UIKIT_EXTERN NSRange const NSRangeNull; 132 | 133 | NSRange NSMakeRangeBetweenLocation(NSUInteger loc1,NSUInteger loc2); 134 | 135 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextCommon.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextCommon.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/9/1. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWCoreTextCommon.h" 10 | #import "DWCoreTextLabelCalculator.h" 11 | 12 | #pragma mark --- DWPosition --- 13 | DWPosition const DWPositionZero = {0,0,0,0}; 14 | 15 | DWPosition const DWPositionNull = {MAXFLOAT,MAXFLOAT,MAXFLOAT,MAXFLOAT}; 16 | 17 | BOOL DWPositionIsNull(DWPosition p) { 18 | return DWPositionEqualToPosition(p, DWPositionNull); 19 | } 20 | 21 | BOOL DWPositionIsZero(DWPosition p) { 22 | return DWPositionEqualToPosition(p, DWPositionZero); 23 | } 24 | 25 | NSComparisonResult DWComparePosition(DWPosition p1,DWPosition p2) { 26 | if (p1.index == p2.index) { 27 | return NSOrderedSame; 28 | } else if (p1.index < p2.index) { 29 | return NSOrderedAscending; 30 | } 31 | return NSOrderedDescending; 32 | } 33 | 34 | BOOL DWPositionBaseOriginEqualToPosition(DWPosition p1,DWPosition p2) { 35 | CGPoint pA = DWPositionGetBaseOrigin(p1); 36 | CGPoint pB = DWPositionGetBaseOrigin(p2); 37 | return CGPointEqualToPoint(pA, pB); 38 | } 39 | 40 | CGRect CGRectFromPosition(DWPosition p,CGFloat width) { 41 | if (DWPositionIsNull(p) || width == CGFLOAT_MAX) { 42 | return CGRectNull; 43 | } 44 | return CGRectMake(p.xCrd, p.baseLineY - p.height, width, p.height); 45 | } 46 | 47 | #pragma mark --- NSRange --- 48 | NSRange const NSRangeZero = {0,0}; 49 | 50 | NSRange const NSRangeNull = {MAXFLOAT,MAXFLOAT}; 51 | 52 | NSRange NSMakeRangeBetweenLocation(NSUInteger loc1,NSUInteger loc2) { 53 | if (loc1 > loc2) { 54 | NSUInteger temp = loc1; 55 | loc1 = loc2; 56 | loc2 = temp; 57 | } 58 | return NSMakeRange(loc1, loc2 - loc1); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextLabel.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLabel.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 16/12/4. 6 | // Copyright © 2016年 Wicky. All rights reserved. 7 | // 8 | 9 | /** 10 | DWCoreTextLabel 11 | 以coreText形式实现的label控件 12 | 13 | version 1.0.0 14 | 实现基本文本展示相关属性 15 | 重写sizeToFit、sizeThatFits:方法 16 | 17 | version 1.0.1 18 | 移除sizeToFit、sizeThatFits:方法 19 | 添加排除区域组 20 | 添加自动重绘标签 21 | 添加绘制图片api 22 | 23 | version 1.0.2 24 | 实现行数限制 25 | 实现尾部省略号折行 26 | 27 | version 1.0.3 28 | 实现插入图片模式 29 | 30 | version 1.0.4 31 | 添加图片点击事件及文字点击事件 32 | 添加活跃文本样式 33 | 34 | version 1.0.5 35 | 修复文本点击换行失效问题 36 | 37 | version 1.0.6 38 | 优化文本计算逻辑 39 | 40 | version 1.0.7 41 | 添加高亮状态、完善高亮逻辑、恢复exclusionP以修复排除区域点击状态下bug 42 | 43 | version 1.0.8 44 | 修复多链接响应排序、高亮等问题 45 | 46 | version 1.0.9 47 | 优化重绘算法 48 | 49 | version 1.0.10 50 | 优化绘制图片算法,添加以路径绘制图片,优化图片判断点击算法,以路径判断 51 | 添加按路径生成图片接口 52 | 53 | version 1.0.11 54 | 绘制图片时添加margin预留参数,调整图片绘制时与文字的内距 55 | 插入图片时添加padding预留参数,调整图片绘制时与两侧文字的外距 56 | 57 | version 1.0.12 58 | 调整代码,解决内存泄漏 59 | 60 | version 1.0.13 61 | 重写sizeToFit、sizeThatFits:方法 62 | 63 | version 1.0.14 64 | 解决初次绘制问题 65 | 66 | version 1.0.15 67 | 添加自动检测链接属性 68 | 添加代理,提供自动链接点击代理 69 | 初步完成电话号码自动检测 70 | 71 | version 1.0.16 72 | 部分赋值重绘规则优化,减少不必要重绘 73 | 添加email、URL自动链接识别 74 | 优化自动链接与活跃文本的优先级:活跃文本>email>URL>phoneNo 75 | 76 | version 1.0.17 77 | 添加自定制检测规则 78 | 自动链接添加自然数类型 79 | 当前链接优先级:活跃文本>自定制检测规则>email>URL>phoneNo>naturalNum 80 | 调整代码分块 81 | 82 | version 1.0.18 83 | 修复自动链接检测高亮范围bug 84 | 修改URL类型自动链接检测规则 85 | 添加自定制检测链接默认属性 86 | 87 | version 1.0.19 88 | 修复垂直对齐bug 89 | 修复取消自动链接逻辑 90 | 优化自动链接匹配逻辑,仅匹配可见文本 91 | 92 | version 1.1.0 93 | 全面支持自动链接支持、定制检测规则、图文混排、响应事件 94 | 优化大部分算法,提高响应效率及绘制效率 95 | 96 | version 1.1.1 97 | 高亮取消逻辑优化 98 | 自动检测逻辑优化 99 | 部分常用方法改为内联函数,提高运行效率 100 | 101 | version 1.1.2 102 | 绘制逻辑优化,改为异步绘制(源码修改自YYTextAsyncLayer) 103 | 104 | version 1.1.3 105 | 异步绘制改造完成、去除事务管理类,事务管理类仍可改进,进行中 106 | 107 | version 1.1.4 108 | 事务管理类去除,异步绘制文件抽出 109 | 110 | version 1.1.5 111 | 添加网络图片现在库,支持绘制网络图片。 112 | 113 | version 1.1.6 114 | 修复layer.contentScale引起的模糊问题 115 | 116 | version 1.1.7 117 | 网络图片绘制api改变,添加占位图接口 118 | 日后关注562行代码前后(暂修复562行前后mAStr地址发生改变问题,若再次复现,追踪562) 119 | 120 | version 1.1.8 121 | 绘制加锁,修复#562的bug(本质原因竞态) 122 | 添加安全释放宏 123 | 修复重绘时重新计算及检测标识符改变逻辑,防止计算及检测需求被抵消 124 | 125 | version 1.2.0 126 | 与pod保持相同版本号 127 | 128 | version 1.2.1 129 | 修复无文本崩溃问题 130 | 更换加锁绘制方式为GCD Barried 131 | 改造排除区域算法 132 | -sizeThatFits:及-sizeToFit改造完成 133 | 修复由textInset上下数值不一样时排除区域偏移的bug 134 | 改以最后一行确定最佳范围为遍历CTRun尺寸并集 135 | 136 | version 1.2.2 137 | 添加DWCoreTextLayout布局类,统一管理布局信息 138 | 修改处理末尾省略号位置,防止插入图片后末尾省略号失效,并去除-sizeThatFits:中对末尾省略号的处理,目前认为其不影响实际尺寸计算 139 | 修改活跃文字、自动链接响应方式。修复设置高亮状态、未设置普通状态时点击后不恢复的bug 140 | 修复设置链接属性字号不改变bug,图片响应方式改造完成 141 | 删除图片API添加完成,父对象弱引用改造完成 142 | Layout计算类计算选中尺寸API添加完成 143 | 选择视图添加完成 144 | 选择视图省略号相关修复、Calculator中DWFixRectToXCrd函数bug修复 145 | 选择动作目录添加完成 146 | 修复insertImage无效问题 147 | 修复存在排除区域时,且numberOfLines所需实际范围小于视图范围是多绘制一行的第一个字的bug 148 | 修复插入图片绘制问题 149 | 150 | (已知bug: 151 | 当排除区域与边界距离小于一个字但大于0时非矩形排除区域会在边界绘制一个字宽。 152 | 两种排除区域方式均无法避免此bug。 153 | 暂怀疑为coreText系统bug。 154 | 这将影响drawImage的surroundMode,此处建议解决方案为配合exclusionPaths中包含同区域矩形path进行解决) 155 | 156 | 少数情况末尾省略号失效,尚未找到原因 157 | 158 | version 1.2.3 159 | 规范Block写法 160 | 161 | version 1.2.4 162 | 修改图片下载类 163 | 164 | */ 165 | 166 | #import 167 | 168 | typedef NS_ENUM(NSUInteger, DWTextVerticalAlignment) {///纵向对齐方式 169 | DWTextVerticalAlignmentCenter, 170 | DWTextVerticalAlignmentTop, 171 | DWTextVerticalAlignmentBottom 172 | }; 173 | 174 | typedef NS_ENUM(NSInteger,DWImageClipMode)//图片剪裁模式 175 | { 176 | DWImageClipModeScaleAspectFit,//适应模式 177 | DWImageClipModeScaleAspectFill,//填充模式 178 | DWImageClipModeScaleToFill//拉伸模式 179 | }; 180 | 181 | typedef NS_ENUM(NSUInteger, DWTextImageDrawMode) {///绘制模式 182 | DWTextImageDrawModeSurround,///环绕模式(控件中包含环绕模式图片后对其设置失效) 183 | DWTextImageDrawModeCover///覆盖模式 184 | }; 185 | 186 | typedef NS_ENUM(NSUInteger, DWLinkType) {///自动链接样式 187 | DWLinkTypeNaturalNum,///自然数 188 | DWLinkTypePhoneNo,///手机号 189 | DWLinkTypeURL,///URL 190 | DWLinkTypeEmail,///Email 191 | DWLinkTypeCustom///自定制 192 | }; 193 | 194 | #define DWDefaultAttributes @{NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),NSForegroundColorAttributeName:[UIColor blueColor]} 195 | #define DWDefaultHighlightAttributes @{NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),NSForegroundColorAttributeName:[UIColor redColor]} 196 | 197 | @class DWCoreTextLabel; 198 | @protocol DWCoreTextLabelDelegate 199 | 200 | @optional 201 | 202 | ///自动链接回调 203 | -(void)coreTextLabel:(DWCoreTextLabel *)label didSelectLink:(NSString *)link range:(NSRange)range linkType:(DWLinkType)linkType; 204 | 205 | @end 206 | 207 | @interface DWCoreTextLabel : UIView 208 | 209 | #pragma mark --- 基本属性 --- 210 | ///代理 211 | @property (nonatomic ,weak) id delegate; 212 | 213 | ///普通文本(注:请先设置text再进行插入图片、链接等操作,设置text会取消之前的操作) 214 | @property (nonatomic ,strong) NSString * text; 215 | 216 | ///文本区域内距 217 | @property (nonatomic ,assign) UIEdgeInsets textInsets; 218 | 219 | ///文本颜色 220 | @property (nonatomic ,strong) UIColor * textColor; 221 | 222 | ///行数 223 | @property (nonatomic ,assign) NSUInteger numberOfLines; 224 | 225 | ///断行模式 226 | @property (nonatomic ,assign) NSLineBreakMode lineBreakMode; 227 | 228 | ///字体 229 | @property (nonatomic ,strong) UIFont * font; 230 | 231 | ///富文本(注:请先设置attributedText再进行插入图片、链接等操作,设置text会取消之前的操作) 232 | @property (nonatomic ,strong) NSAttributedString * attributedText; 233 | 234 | ///水平对齐方式 235 | @property (nonatomic ,assign) NSTextAlignment textAlignment; 236 | 237 | ///垂直对齐方式 238 | @property (nonatomic ,assign) DWTextVerticalAlignment textVerticalAlignment; 239 | 240 | ///行间距 241 | @property (nonatomic ,assign) CGFloat lineSpacing; 242 | 243 | /** 244 | 排除区域组 245 | 246 | 注: 247 | 设置排除区域后,对齐方式失效 248 | */ 249 | @property (nonatomic ,strong) NSArray * exclusionPaths; 250 | 251 | 252 | /** 253 | 排除子视图区域 254 | 255 | 注: 256 | 1.若为真且子视图数量非0,对齐方式失效。 257 | 2.默认开启 258 | 3.开启后在重绘时自动排除。 259 | */ 260 | @property (nonatomic ,assign) BOOL excludeSubviews; 261 | 262 | /** 263 | 自动重绘 264 | 265 | 默认关闭,开启后设置需要重绘的属性后自动重绘 266 | */ 267 | @property (nonatomic ,assign) BOOL autoRedraw; 268 | 269 | ///活跃文本的属性 270 | @property (nonatomic ,strong) NSDictionary * activeTextAttributes; 271 | 272 | ///活跃文本的高亮属性 273 | @property (nonatomic ,strong) NSDictionary * activeTextHighlightAttributes; 274 | 275 | /** 276 | 自动检测特殊链接 277 | 278 | 注: 279 | 1.包括手机号码 280 | 2.默认关闭 281 | */ 282 | @property (nonatomic ,assign) BOOL autoCheckLink; 283 | 284 | /** 285 | 自动检测的配置字典 286 | 287 | 用于定制检测规则 288 | 可修改本配置字典以达到改变检测规则或减少检测项 289 | 290 | 注: 291 | 1.可修改检测规则,及正则文本,不可添加或改变键名 292 | 2.可减少键值对,不可增加预置类型外的键值对 293 | 3.若想匹配预置类型外的规则,请使用customLinkRegex属性 294 | */ 295 | @property (nonatomic ,strong) NSMutableDictionary * autoCheckConfig; 296 | 297 | /** 298 | 匹配自定制特殊链接的正则 299 | */ 300 | @property (nonatomic ,copy) NSString * customLinkRegex; 301 | 302 | #pragma mark --- 自动连接属性 --- 303 | ///以下属性在autoCheckLink为真时有效 304 | 305 | /** 306 | 自然数属性 307 | */ 308 | @property (nonatomic ,strong) NSDictionary * naturalNumAttributes; 309 | 310 | /** 311 | 自然数高亮属性 312 | */ 313 | @property (nonatomic ,strong) NSDictionary * naturalNumHighlightAttributes; 314 | 315 | /** 316 | 电话号码属性 317 | */ 318 | @property (nonatomic ,strong) NSDictionary * phoneNoAttributes; 319 | 320 | /** 321 | 电话号码高亮时属性 322 | */ 323 | @property (nonatomic ,strong) NSDictionary * phoneNoHighlightAttributes; 324 | 325 | /** 326 | URL属性 327 | */ 328 | @property (nonatomic ,strong) NSDictionary * URLAttributes; 329 | 330 | /** 331 | URL高亮时属性 332 | */ 333 | @property (nonatomic ,strong) NSDictionary * URLHighlightAttributes; 334 | 335 | /** 336 | email属性 337 | */ 338 | @property (nonatomic ,strong) NSDictionary * emailAttributes; 339 | 340 | /** 341 | email高亮时属性 342 | */ 343 | @property (nonatomic ,strong) NSDictionary * emailHighlightAttributes; 344 | 345 | /** 346 | 自定制链接属性 347 | */ 348 | @property (nonatomic ,strong) NSDictionary * customLinkAttributes; 349 | 350 | /** 351 | 自定制链接高亮时属性 352 | */ 353 | @property (nonatomic ,strong) NSDictionary * customLinkHighlightAttributes; 354 | 355 | /** 356 | 选择模式 357 | */ 358 | @property (nonatomic ,assign ,readonly) BOOL selectingMode; 359 | 360 | /** 361 | 允许选中 362 | */ 363 | @property (nonatomic ,assign) BOOL allowSelect; 364 | 365 | #pragma mark --- 接口方法 --- 366 | /** 367 | 以frame绘制矩形图片 368 | 369 | image 将要绘制的图片 370 | imageID 图片的唯一标示,用于删除图片 371 | url 网络图片的地址 372 | placeHolder 网络图片下载完成之前的占位图 373 | frame 绘制图片的尺寸 374 | margin 绘制图片时的内距效果,margin大于0时,绘制的实际尺寸小于frame,同时保证绘制中心不变 375 | drawMode 图片绘制模式,包括环绕型和覆盖型 376 | target 可选参数,与target配套使用 377 | selector 为图片添加点击事件,响应区域为图片实际绘制区域 378 | */ 379 | -(void)dw_DrawImage:(UIImage *)image withImageID:(NSString *)imageID atFrame:(CGRect)frame margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 380 | 381 | -(void)dw_DrawImageWithUrl:(NSString *)url withImageID:(NSString *)imageID atFrame:(CGRect)frame margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 382 | 383 | -(void)dw_DrawImageWithUrl:(NSString *)url withImageID:(NSString *)imageID placeHolder:(UIImage *)placeHolder atFrame:(CGRect)frame margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 384 | 385 | /** 386 | 以path绘制不规则形状图片 387 | 388 | image 将要绘制的图片 389 | imageID 图片的唯一标示,用于删除图片 390 | url 网络图片的地址 391 | placeHolder 网络图片下载完成之前的占位图 392 | path 绘制图片的路径,先以path对图片进行剪裁,后绘制 393 | margin 绘制图片时的内距效果,margin大于0时,绘制的实际路径小于path,同时保证绘制中心不变 394 | drawMode 图片绘制模式,包括环绕型和覆盖型 395 | target 可选参数,与target配套使用 396 | selector 为图片添加点击事件,响应区域为图片实际绘制区域 397 | 398 | 注: 399 | 1.将自动按照path路径形状剪裁图片,图片无需事先处理 400 | 2.自动剪裁图片时按照path的形状剪裁,与origin无关。 401 | */ 402 | -(void)dw_DrawImage:(UIImage *)image withImageID:(NSString *)imageID path:(UIBezierPath *)path margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 403 | 404 | -(void)dw_DrawImageWithUrl:(NSString *)url withImageID:(NSString *)imageID path:(UIBezierPath *)path margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 405 | 406 | -(void)dw_DrawImageWithUrl:(NSString *)url withImageID:(NSString *)imageID placeHolder:(UIImage *)placeHolder path:(UIBezierPath *)path margin:(CGFloat)margin drawMode:(DWTextImageDrawMode)mode target:(id)target selector:(SEL)selector; 407 | 408 | /** 409 | 插入图片 410 | 411 | image 将要绘制的图片 412 | imageID 图片的唯一标示,用于删除图片 413 | url 网络图片的地址 414 | placeHolder 网络图片下载完成之前的占位图 415 | size 绘制图片的大小 416 | padding 绘制图片时的外距效果,padding大于0时,绘制的图片距两端文字有padding的距离 417 | descent 绘制图片的底部基线与文字基线的偏移量,当descent等于0时,与文字的底部基线对其 418 | location 要插入图片的位置 419 | target 可选参数,与target配套使用 420 | selector 为图片添加点击事件,响应区域为图片实际绘制区域 421 | 422 | 注: 423 | 1.在指定位置插入图片,图片大小会影响行间距 424 | 2.插入图片的位置不影响为范围内文字添加点击事件,无需另做考虑 425 | 3.由于CoreText计算原理导致,有些情况下插入大图绘制时由于绘制区域无法完整绘制大图导致绘制错误,插入位置后字符串的会无法显示。故绘制大图建议采用dw_DrawImage系列API进行绘制。插入系列更适合聊天文字中穿插表情等相似情景。 426 | */ 427 | -(void)dw_InsertImage:(UIImage *)image withImageID:(NSString *)imageID size:(CGSize)size padding:(CGFloat)padding descent:(CGFloat)descent atLocation:(NSUInteger)location target:(id)target selector:(SEL)selector; 428 | 429 | -(void)dw_InsertImageWithUrl:(NSString *)url withImageID:(NSString *)imageID size:(CGSize)size padding:(CGFloat)padding descent:(CGFloat)descent atLocation:(NSUInteger)location target:(id)target selector:(SEL)selector; 430 | 431 | -(void)dw_InsertImageWithUrl:(NSString *)url withImageID:(NSString *)imageID placeHolder:(UIImage *)placeHolder size:(CGSize)size padding:(CGFloat)padding descent:(CGFloat)descent atLocation:(NSUInteger)location target:(id)target selector:(SEL)selector; 432 | 433 | 434 | /** 435 | 删除图片 436 | 437 | @param imageID 删除图片的图片标识 438 | */ 439 | -(void)dw_RemoveImageByID:(NSString *)imageID; 440 | 441 | /** 442 | 为指定区域文本添加点击事件 443 | 444 | @param target 响应者 445 | @param selector 响应事件 446 | @param range 添加事件的区域 447 | 448 | 注:此处无需考虑插入图片位置的影响,无论之前插入范围还是之后插入范围,只需传入插入图片之前文字范围即可。API将自动对文字事件范围做偏移校正。 449 | */ 450 | -(void)dw_AddTarget:(id)target selector:(SEL)selector toRange:(NSRange)range; 451 | 452 | /** 453 | 返回指定形状的image对象 454 | */ 455 | 456 | 457 | /** 458 | 返回指定形状的image对象 459 | 460 | image 需要剪裁的图片 461 | url 网络图片的地址 462 | path 剪裁的路径 463 | mode 图片填充的模式 464 | */ 465 | +(UIImage *)dw_ClipImage:(UIImage *)image withPath:(UIBezierPath *)path mode:(DWImageClipMode)mode; 466 | 467 | +(void)dw_ClipImageWithUrl:(NSString *)url withPath:(UIBezierPath *)path mode:(DWImageClipMode)mode completion:(void(^)(UIImage * image))completion; 468 | 469 | /** 470 | 返回限制尺寸下当前视图最合适的尺寸 471 | 472 | @param size 限制尺寸 473 | @return 最佳尺寸 474 | 475 | 注:限制尺寸应尽量选择大概近似数字,计算速度直接取决于限制尺寸,尺寸越大计算耗时越长,切勿填写MAXFLOAT。 476 | */ 477 | -(CGSize)sizeThatFits:(CGSize)size; 478 | 479 | 480 | /** 481 | 自动设置为当前视图最合适的尺寸 482 | 483 | 注:此函数基于-sizeThatFits:,故其特性与注意事项与-sizeThatFits:相同 484 | */ 485 | -(void)sizeToFit; 486 | 487 | @end 488 | 489 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextLabelCalculator.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLabelCalculator.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/7/20. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | @class DWCoreTextLabel; 12 | @class DWCoreTextLayout; 13 | 14 | ///安全释放 15 | #define CFSAFERELEASE(a)\ 16 | do {\ 17 | if(a) {\ 18 | CFRelease(a);\ 19 | a = NULL;\ 20 | }\ 21 | } while(0); 22 | 23 | ///安全赋值 24 | #define CFSAFESETVALUEA2B(a,b)\ 25 | do {\ 26 | CFSAFERELEASE(b)\ 27 | if (a) {\ 28 | CFRetain(a);\ 29 | b = a;\ 30 | }\ 31 | } while(0); 32 | 33 | 34 | #pragma mark --- 获取相关数据 --- 35 | /** 36 | 获取当前需要绘制的文本 37 | 38 | @param label Label控件 39 | @param limitWidth 宽度限制 40 | @param exclusionPaths 排除区域数组 41 | @return 要绘制的字符串 42 | 43 | 注:此处仅处理段落样式、字体、颜色、对齐方式,不处理高亮、插入图片、文末省略 44 | */ 45 | NSMutableAttributedString * getMAStr(DWCoreTextLabel * label,CGFloat limitWidth,NSArray * exclusionPaths); 46 | 47 | /** 48 | 获取插入图片偏移量 49 | 50 | @param locations 已插入数组 51 | @param newLoc 目标插入位置(不考虑偏移) 52 | @return 实际插入位置(偏移校正后) 53 | */ 54 | NSInteger getInsertOffset(NSMutableArray * locations,NSInteger newLoc); 55 | 56 | /** 57 | 获取建议绘制尺寸 58 | 59 | @param frameSetter 获取大致绘制尺寸 60 | @param rangeToDraw 需要绘制的范围 61 | @param limitWidth 宽度限制 62 | @param numberOfLines 行数 63 | @return 建议绘制尺寸 64 | 65 | 注:仅在无排除区域时计算有效 66 | */ 67 | CGSize getSuggestSize(CTFramesetterRef frameSetter,CFRange rangeToDraw,CGFloat limitWidth,NSUInteger numberOfLines); 68 | 69 | 70 | /** 71 | 转换range 72 | 73 | @param range CFRange 74 | @return NSRange 75 | */ 76 | NSRange NSRangeFromCFRange(CFRange range); 77 | 78 | /** 79 | 获取计算绘制可见文本范围 80 | 81 | @param frame 绘制frame 82 | @return 返回可见范围 83 | */ 84 | CFRange getRangeToDrawForVisibleString(CTFrameRef frame); 85 | 86 | /** 87 | 获取最后一行绘制范围 88 | 89 | @param frame 绘制frame 90 | @param numberOfLines 行数 91 | @param visibleRange 可见范围 92 | @return 最后一行范围 93 | */ 94 | CFRange getLastLineRange(CTFrameRef frame,NSUInteger numberOfLines,CFRange visibleRange); 95 | 96 | /** 97 | 通过最后一行区域获取修正后的可见区域 98 | 99 | @param visibleRange 当前可见区域 100 | @param lastRange 当前最后一行区域 101 | @return 修正后的可见区域 102 | */ 103 | CFRange getVisibleRangeFromLastRange(CFRange visibleRange,CFRange lastRange); 104 | 105 | /** 106 | 根据margin获取图片实际响应区域 107 | 108 | @param path 图片围绕区域 109 | @param margin 图片围绕区域内缩进 110 | @return 图片实际响应区域 111 | */ 112 | UIBezierPath * getImageAcitvePath(UIBezierPath * path,CGFloat margin); 113 | 114 | 115 | /** 116 | 根据CTFrame及转换视图高度获取将要绘制的frame 117 | 118 | @param ctFrame 计算用的绘制Frame 119 | @param height 视图高度 120 | @param startFromZero 是否从0开始计算 121 | @return 将要绘制的尺寸 122 | 123 | 注: 124 | sizeThatFits中从零开始计算,正常绘制时不从零计算 125 | */ 126 | CGRect getDrawFrame(CTFrameRef ctFrame,CGFloat height,BOOL startFromZero); 127 | 128 | /** 129 | 获取CTRun对应的实际尺寸 130 | 131 | @param frame 绘制frame 132 | @param line CTRun所在CTLine 133 | @param origin CTLine对应原点 134 | @param run CTRun 135 | @return 返回CTRun对应的实际尺寸 136 | 137 | 注:此坐标为系统坐标,需于屏幕坐标进行转换 138 | */ 139 | CGRect getCTRunBounds(CTFrameRef frame,CTLineRef line,CGPoint origin,CTRunRef run); 140 | 141 | /** 142 | 获取尺寸相对于Frame的路径校正后的尺寸 143 | 144 | @param rect 原始尺寸 145 | @param frame 对应CTFrame 146 | @return 校正后尺寸 147 | */ 148 | CGRect getRectWithCTFramePathOffset(CGRect rect,CTFrameRef frame); 149 | 150 | /** 151 | 获取Frame的路径的横坐标偏移 152 | 153 | @param frame 对应CTFrame 154 | @return 横坐标偏移量 155 | */ 156 | CGFloat getCTFramePahtXOffset(CTFrameRef frame); 157 | 158 | /** 159 | 获取活动图片中包含点的字典 160 | 161 | @param arr 所有活动图片配置的数组 162 | @param point 当前点击的point 163 | @return 返回当前点击的点对应的活动图片配置 164 | */ 165 | NSMutableDictionary * getImageDic(NSArray * arr,CGPoint point); 166 | 167 | /** 168 | 根据插入图片的位置对range的偏移进行校正 169 | 170 | @param range 原始range 171 | @param arrLocationImgHasAdd 插入图片位置的数组 172 | @return 校正后的range 173 | */ 174 | NSRange getRangeOffset(NSRange range,NSMutableArray * arrLocationImgHasAdd); 175 | 176 | /** 177 | 返回目标范围排除指定范围后的结果数组 178 | 179 | @param targetRange 目标范围 180 | @param exceptRange 需要排除的范围 181 | @return 返回排除范围后的范围 182 | */ 183 | NSArray * getRangeExcept(NSRange targetRange,NSRange exceptRange); 184 | 185 | /** 186 | 返回排除区域字典 187 | 188 | @param paths 需要排除的区域数组 189 | @param viewBounds 实际绘制bounds 190 | @return 排除区域的配置字典 191 | */ 192 | NSDictionary * getExclusionDic(NSArray * paths,CGRect viewBounds); 193 | 194 | /** 195 | 将给定数组中的路径根据偏移量校正路径后放入指定容器 196 | 197 | @param container 指定容器 198 | @param pathArr 给定路径数组 199 | @param offset 纵向偏移量 200 | */ 201 | void handleExclusionPathArr(NSMutableArray * container,NSArray * pathArr,CGFloat offset); 202 | 203 | 204 | #pragma mark --- 镜像转换方法 --- 205 | /** 206 | 获取镜像path 207 | 208 | @param path 原始路径 209 | @param bounds 需要镜像的尺寸 210 | */ 211 | void convertPath(UIBezierPath * path,CGRect bounds); 212 | 213 | /** 214 | 获取镜像frame 215 | 216 | @param rect 原始尺寸 217 | @param height 需要镜像的高度 218 | @return 镜像后的尺寸 219 | */ 220 | CGRect convertRect(CGRect rect,CGFloat height); 221 | 222 | /** 223 | 平移路径 224 | 225 | @param path 原始路径 226 | @param offsetY 纵向平移距离 227 | */ 228 | void translatePath(UIBezierPath * path,CGFloat offsetY); 229 | 230 | 231 | #pragma mark --- 比较方法 --- 232 | /** 233 | 返回给定数在所给范围中的相对位置 234 | 235 | @param num 给定数 236 | @param a 范围A 237 | @param b 范围B 238 | @return 返回相对位置 239 | 240 | 注: 241 | 当num在数轴上位于[a,b]左侧时返回NSOrderedAscending,右侧返回NSOrderedDescending,包含关系返回NSOrderedSame 242 | */ 243 | NSComparisonResult DWNumBetweenAB(CGFloat num,CGFloat a,CGFloat b); 244 | 245 | 246 | #pragma mark --- 空间位置关系方法 --- 247 | /** 248 | 返指给定点在给定尺寸中的竖直位置关系 249 | 250 | @param point 指定点 251 | @param rect 给定尺寸 252 | @return 相对竖直位置 253 | 254 | 注: 255 | 返回结果定义同DWNumBetweenAB() 256 | */ 257 | NSComparisonResult DWPointInRectV(CGPoint point,CGRect rect); 258 | 259 | /** 260 | 返指给定点在给定尺寸中的水平位置关系 261 | 262 | @param point 指定点 263 | @param rect 给定尺寸 264 | @return 相对水平位置 265 | 266 | 注: 267 | 返回结果定义同DWNumBetweenAB() 268 | */ 269 | NSComparisonResult DWPointInRectH(CGPoint point,CGRect rect); 270 | 271 | /** 272 | 返回距离指定坐标较近的一侧的坐标值 273 | 274 | @param xCrd 指定坐标 275 | @param left 左侧坐标 276 | @param right 右侧坐标 277 | @return 较近一侧的坐标 278 | 279 | 注: 280 | 当左右坐标传入空间位置矛盾时会自动交换左右坐标 281 | */ 282 | CGFloat DWClosestSide(CGFloat xCrd,CGFloat left,CGFloat right); 283 | 284 | /** 285 | 返回给定点是否在给定尺寸的修正范围内 286 | 287 | @param rect 给定尺寸 288 | @param point 给定点 289 | @return 是否包含 290 | */ 291 | BOOL DWRectFixContainsPoint(CGRect rect,CGPoint point); 292 | 293 | /** 294 | 比较指定坐标在给定尺寸中的位置 295 | 296 | @param xCrd 指定坐标 297 | @param rect 给定尺寸 298 | @return 相对位置 299 | 300 | 注: 301 | 返回结果定义同DWNumBetweenAB() 302 | */ 303 | NSComparisonResult DWCompareXCrdWithRect(CGFloat xCrd,CGRect rect); 304 | 305 | 306 | #pragma mark --- 尺寸修正方法 --- 307 | /** 308 | 缩短CGRect至指定坐标 309 | 310 | @param rect 原始尺寸 311 | @param xCrd 指定x坐标 312 | @param backward 是否为向后模式 313 | @return 缩短后的尺寸 314 | 315 | 注: 316 | 向后模式及保留指定x坐标右侧区域,反之亦然 317 | */ 318 | CGRect DWShortenRectToXCrd(CGRect rect,CGFloat xCrd,BOOL backward); 319 | 320 | /** 321 | 延长尺寸至指定坐标 322 | 323 | @param rect 原始尺寸 324 | @param xCrd 指定坐标 325 | @return 延长后的尺寸 326 | */ 327 | CGRect DWLengthenRectToXCrd(CGRect rect,CGFloat xCrd); 328 | 329 | 330 | /** 331 | 比较两个点的空间位置 332 | 333 | @param p1 点1 334 | @param p2 点2 335 | @return 返回点2相对于点1的位置 336 | 337 | 注: 338 | 当p1与p2重合时返回NSOrderedSame, 339 | 当向量p1p2与坐标系x轴夹角位于(Pi,2Pi]时返回NSOrderedAscending, 340 | 其余情况返回NSOrderedDescending。 341 | */ 342 | NSComparisonResult DWComparePoint(CGPoint p1,CGPoint p2); 343 | 344 | #pragma mark --- 尺寸组合方法 --- 345 | 346 | /** 347 | 返回target中不在origin范围内的尺寸集合 348 | 349 | @param target 待分隔的尺寸 350 | @param origin 分隔参照的尺寸 351 | @return target中不在origin范围内的尺寸集合 352 | 353 | 注: 354 | 当target包含origin是返回nil。 355 | 当origin包含target是返回空数组。 356 | 当没有交集是返回target 357 | 其余返回不在origin范围内的尺寸集合 358 | */ 359 | NSArray * DWRectsBeyondRect(CGRect target,CGRect origin); 360 | 361 | #pragma mark --- 交换对象方法 --- 362 | ///交换两个浮点数 363 | void DWSwapfAB(CGFloat *a,CGFloat *b); 364 | 365 | ///交换两个对象 366 | void DWSwapoAB(id a,id b); 367 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextLabelCalculator.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLabelCalculator.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/7/20. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWCoreTextLabelCalculator.h" 10 | #import "DWCoreTextLabel.h" 11 | #import "DWCoreTextLayout.h" 12 | 13 | #pragma mark --- 获取相关数据 --- 14 | ///获取当前需要绘制的文本 15 | NSMutableAttributedString * getMAStr(DWCoreTextLabel * label,CGFloat limitWidth,NSArray * exclusionPaths) { 16 | NSMutableAttributedString * mAStr = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText]; 17 | NSUInteger length = label.attributedText?label.attributedText.length:label.text.length; 18 | NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 19 | NSRange totalRange = NSMakeRange(0, length); 20 | if (!label.attributedText) { 21 | [paragraphStyle setLineBreakMode:label.lineBreakMode]; 22 | [paragraphStyle setLineSpacing:label.lineSpacing]; 23 | paragraphStyle.alignment = (exclusionPaths.count == 0)?label.textAlignment:NSTextAlignmentLeft; 24 | NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:label.text]; 25 | [attributeStr addAttribute:NSFontAttributeName value:label.font range:totalRange]; 26 | [attributeStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:totalRange]; 27 | [attributeStr addAttribute:NSForegroundColorAttributeName value:label.textColor range:totalRange]; 28 | mAStr = attributeStr; 29 | } else { 30 | [paragraphStyle setLineBreakMode:label.lineBreakMode]; 31 | paragraphStyle.alignment = (exclusionPaths.count == 0)?label.textAlignment:NSTextAlignmentLeft; 32 | [mAStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:totalRange]; 33 | } 34 | return mAStr; 35 | } 36 | 37 | ///获取插入图片偏移量 38 | NSInteger getInsertOffset(NSMutableArray * locations,NSInteger newLoc) { 39 | NSNumber * loc = [NSNumber numberWithInteger:newLoc]; 40 | if (locations.count == 0) {//如果数组是空的,直接添加位置,并返回0 41 | [locations addObject:loc]; 42 | return 0; 43 | } 44 | [locations addObject:loc];//否则先插入,再排序 45 | [locations sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {//升序排序方法 46 | if ([obj1 integerValue] > [obj2 integerValue]) { 47 | return (NSComparisonResult)NSOrderedDescending; 48 | } 49 | 50 | if ([obj1 integerValue] < [obj2 integerValue]) { 51 | return (NSComparisonResult)NSOrderedAscending; 52 | } 53 | return (NSComparisonResult)NSOrderedSame; 54 | }]; 55 | return [locations indexOfObject:loc];//返回本次插入图片的偏移量 56 | } 57 | 58 | ///获取绘制尺寸 59 | CGSize getSuggestSize(CTFramesetterRef frameSetter,CFRange rangeToDraw,CGFloat limitWidth,NSUInteger numberOfLines) { 60 | CGSize restrictSize = CGSizeMake(limitWidth, MAXFLOAT); 61 | if (numberOfLines == 1) { 62 | restrictSize = CGSizeMake(MAXFLOAT, MAXFLOAT); 63 | } 64 | CGSize suggestSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, rangeToDraw, NULL, restrictSize, nil); 65 | return CGSizeMake(ceil(MIN(suggestSize.width, limitWidth)),ceil(suggestSize.height)); 66 | } 67 | 68 | NSRange NSRangeFromCFRange(CFRange range) { 69 | return NSMakeRange(range.location, range.length); 70 | } 71 | 72 | ///获取计算绘制可见文本范围 73 | CFRange getRangeToDrawForVisibleString(CTFrameRef frame) { 74 | return CTFrameGetVisibleStringRange(frame); 75 | } 76 | 77 | ///获取最后一行绘制范围 78 | CFRange getLastLineRange(CTFrameRef frame ,NSUInteger numberOfLines,CFRange visibleRange) { 79 | CFRange range = CFRangeMake(0, 0); 80 | NSRange vRange = NSMakeRange(visibleRange.location, visibleRange.length); 81 | if (numberOfLines == 0) { 82 | numberOfLines = ULONG_MAX; 83 | } 84 | CFArrayRef lines = CTFrameGetLines(frame); 85 | long lineCount = CFArrayGetCount(lines); 86 | if (lineCount > 0) { 87 | NSUInteger lineNum = 0; 88 | if (numberOfLines <= lineCount) { 89 | lineNum = numberOfLines; 90 | CTLineRef line = CFArrayGetValueAtIndex(lines, lineNum - 1); 91 | range = CTLineGetStringRange(line); 92 | } else { 93 | for (int i = 0; i < lineCount; i++) { 94 | CTLineRef line = CFArrayGetValueAtIndex(lines, i); 95 | 96 | CFRange tempRange = CTLineGetStringRange(line); 97 | if (NSLocationInRange(NSMaxRange(NSMakeRange(tempRange.location, tempRange.length)) - 1,vRange)) { 98 | range = tempRange; 99 | } else { 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | return range; 106 | } 107 | 108 | CFRange getVisibleRangeFromLastRange(CFRange visibleRange,CFRange lastRange) { 109 | CFRange range = CFRangeMake(0, 0); 110 | range.location = MIN(visibleRange.location, lastRange.location); 111 | NSUInteger maxLocVisible = visibleRange.location + visibleRange.length; 112 | NSUInteger maxLocLast = lastRange.location + lastRange.length; 113 | range.length = maxLocVisible < maxLocLast ? maxLocVisible - range.location : maxLocLast - range.location; 114 | return range; 115 | } 116 | 117 | ///获取按照margin缩放的frame 118 | UIBezierPath * getImageAcitvePath(UIBezierPath * path,CGFloat margin) { 119 | UIBezierPath * newPath = [path copy]; 120 | if (margin == 0) { 121 | return newPath; 122 | } 123 | CGFloat widthScale = 1 - margin * 2 / newPath.bounds.size.width; 124 | CGFloat heightScale = 1 - margin * 2 / newPath.bounds.size.height; 125 | CGFloat offsetX = newPath.bounds.origin.x * (1 - widthScale) + margin; 126 | CGFloat offsetY = newPath.bounds.origin.y * (1 -heightScale) + margin; 127 | [newPath applyTransform:CGAffineTransformMakeScale(widthScale, heightScale)]; 128 | [newPath applyTransform:CGAffineTransformMakeTranslation(offsetX, offsetY)]; 129 | return newPath; 130 | } 131 | 132 | ///获取绘制所需尺寸 133 | CGRect getDrawFrame(CTFrameRef ctFrame,CGFloat height,BOOL startFromZero) { 134 | DWCoreTextLayout * layout = [DWCoreTextLayout layoutWithCTFrame:ctFrame convertHeight:height considerGlyphs:NO]; 135 | __block CGRect desFrame = CGRectNull; 136 | if (startFromZero) { 137 | desFrame = CGRectZero; 138 | } 139 | [layout.lines enumerateObjectsUsingBlock:^(DWCTLineWrapper * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 140 | CGRect temp = obj.frame; 141 | if (CGRectEqualToRect(desFrame, CGRectNull)) { 142 | desFrame = temp; 143 | return ; 144 | } 145 | desFrame = CGRectUnion(temp, desFrame); 146 | }]; 147 | return desFrame; 148 | } 149 | 150 | ///获取CTRun的frame 151 | CGRect getCTRunBounds(CTFrameRef frame,CTLineRef line,CGPoint origin,CTRunRef run) { 152 | CGFloat ascent; 153 | CGFloat descent; 154 | CGRect boundsRun = CGRectZero; 155 | boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); 156 | boundsRun.size.height = ascent + descent; 157 | CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); 158 | boundsRun.origin.x = origin.x + xOffset; 159 | boundsRun.origin.y = origin.y - descent; 160 | return getRectWithCTFramePathOffset(boundsRun, frame); 161 | } 162 | 163 | ///获取CTFrame校正后的尺寸 164 | CGRect getRectWithCTFramePathOffset(CGRect rect,CTFrameRef frame) { 165 | CGPathRef path = CTFrameGetPath(frame); 166 | CGRect colRect = CGPathGetBoundingBox(path); 167 | return CGRectOffset(rect, colRect.origin.x, colRect.origin.y); 168 | } 169 | 170 | 171 | 172 | ///获取Frame的路径的横坐标偏移 173 | CGFloat getCTFramePahtXOffset(CTFrameRef frame) { 174 | CGPathRef path = CTFrameGetPath(frame); 175 | CGRect colRect = CGPathGetBoundingBox(path); 176 | return colRect.origin.x; 177 | } 178 | 179 | ///获取活动图片中包含点的字典 180 | NSMutableDictionary * getImageDic(NSArray * arr,CGPoint point) { 181 | __block NSMutableDictionary * dicClicked = nil; 182 | [arr enumerateObjectsUsingBlock:^(NSMutableDictionary * dic, NSUInteger idx, BOOL * _Nonnull stop) { 183 | UIBezierPath * path = dic[@"activePath"]; 184 | if ([path containsPoint:point]) { 185 | if (dic[@"target"] && dic[@"SEL"]) { 186 | dicClicked = dic; 187 | } 188 | *stop = YES; 189 | } 190 | }]; 191 | return dicClicked; 192 | } 193 | 194 | ///矫正range偏移量 195 | NSRange getRangeOffset(NSRange range,NSMutableArray * arrLocationImgHasAdd) { 196 | __block NSRange newRange = range; 197 | [arrLocationImgHasAdd enumerateObjectsUsingBlock:^(NSNumber * location, NSUInteger idx, BOOL * _Nonnull stop) { 198 | if (location.integerValue <= range.location) { 199 | newRange.location ++; 200 | } else if (location.integerValue <= (range.location + range.length - 1)) { 201 | newRange.length ++; 202 | } 203 | }]; 204 | return newRange; 205 | } 206 | 207 | ///返回目标范围排除指定范围后的结果数组 208 | NSArray * getRangeExcept(NSRange targetRange,NSRange exceptRange) { 209 | NSRange interRange = NSIntersectionRange(targetRange, exceptRange); 210 | if (interRange.length == 0) { 211 | return nil; 212 | } else if (NSEqualRanges(targetRange, interRange)) { 213 | return nil; 214 | } 215 | NSMutableArray * arr = [NSMutableArray array]; 216 | 217 | if (interRange.location > targetRange.location) { 218 | [arr addObject:[NSValue valueWithRange:NSMakeRange(targetRange.location, interRange.location - targetRange.location)]]; 219 | } 220 | if (NSMaxRange(targetRange) > NSMaxRange(interRange)) { 221 | [arr addObject:[NSValue valueWithRange:NSMakeRange(NSMaxRange(interRange), NSMaxRange(targetRange) - NSMaxRange(interRange))]]; 222 | } 223 | return arr.copy; 224 | }; 225 | 226 | ///返回排除区域字典 227 | NSDictionary * getExclusionDic(NSArray * paths,CGRect viewBounds) { 228 | if (paths.count == 0) { 229 | return NULL; 230 | } 231 | NSMutableArray *pathsArray = [[NSMutableArray alloc] init]; 232 | [paths enumerateObjectsUsingBlock:^(UIBezierPath * obj, NSUInteger idx, BOOL * _Nonnull stop) { 233 | convertPath(obj, viewBounds); 234 | NSDictionary *clippingPathDictionary = [NSDictionary dictionaryWithObject:(__bridge id)(obj.CGPath) forKey:(__bridge NSString *)kCTFramePathClippingPathAttributeName]; 235 | [pathsArray addObject:clippingPathDictionary]; 236 | }]; 237 | return [NSDictionary dictionaryWithObjectsAndKeys:pathsArray,kCTFrameClippingPathsAttributeName, nil]; 238 | } 239 | 240 | ///将给定数组中的路径根据偏移量校正路径后放入指定容器 241 | void handleExclusionPathArr(NSMutableArray * container,NSArray * pathArr,CGFloat offset) { 242 | [pathArr enumerateObjectsUsingBlock:^(UIBezierPath * obj, NSUInteger idx, BOOL * _Nonnull stop) { 243 | translatePath(obj, offset); 244 | [container addObject:obj]; 245 | }]; 246 | } 247 | 248 | 249 | #pragma mark --- 镜像转换方法 --- 250 | ///获取镜像path 251 | void convertPath(UIBezierPath * path,CGRect bounds) { 252 | [path applyTransform:CGAffineTransformMakeScale(1, -1)]; 253 | [path applyTransform:CGAffineTransformMakeTranslation(0, 2 * bounds.origin.y + bounds.size.height)]; 254 | } 255 | 256 | ///获取镜像frame 257 | CGRect convertRect(CGRect rect,CGFloat height) { 258 | if (CGRectEqualToRect(rect, CGRectNull)) { 259 | return CGRectNull; 260 | } 261 | return CGRectMake(rect.origin.x, height - rect.origin.y - rect.size.height, rect.size.width, rect.size.height); 262 | } 263 | 264 | ///平移路径 265 | void translatePath(UIBezierPath * path,CGFloat offsetY) { 266 | if (offsetY == 0) { 267 | return; 268 | } 269 | [path applyTransform:CGAffineTransformMakeTranslation(0, offsetY)]; 270 | } 271 | 272 | 273 | #pragma mark --- 比较方法 --- 274 | ///在一定精度内判断两个浮点数是否相等 275 | BOOL DWFixEqual(CGFloat a,CGFloat b) { 276 | if (fabs(a - b) < 1e-6) { 277 | return YES; 278 | } 279 | return NO; 280 | } 281 | 282 | ///返回给定数在所给范围中的相对位置 283 | NSComparisonResult DWNumBetweenAB(CGFloat num,CGFloat a,CGFloat b) { 284 | if (a > b) { 285 | DWSwapfAB(&a, &b); 286 | } 287 | if (num < a) { 288 | if (DWFixEqual(a,num)) { 289 | return NSOrderedSame; 290 | } 291 | return NSOrderedAscending; 292 | } else if (num > b) { 293 | if (DWFixEqual(b,num)) { 294 | return NSOrderedSame; 295 | } 296 | return NSOrderedDescending; 297 | } else { 298 | return NSOrderedSame; 299 | } 300 | } 301 | 302 | 303 | #pragma mark --- 空间位置关系方法 --- 304 | ///返指给定点在给定尺寸中的竖直位置关系 305 | NSComparisonResult DWPointInRectV(CGPoint point,CGRect rect) { 306 | return DWNumBetweenAB(point.y, CGRectGetMinY(rect), CGRectGetMaxY(rect)); 307 | } 308 | 309 | ///返指给定点在给定尺寸中的水平位置关系 310 | NSComparisonResult DWPointInRectH(CGPoint point,CGRect rect) { 311 | return DWNumBetweenAB(point.x, CGRectGetMinX(rect), CGRectGetMaxX(rect)); 312 | } 313 | 314 | ///返回距离指定坐标较近的一侧的坐标值 315 | CGFloat DWClosestSide(CGFloat xCrd,CGFloat left,CGFloat right) { 316 | if (right < left) { 317 | DWSwapfAB(&left, &right); 318 | } 319 | CGFloat mid = (left + right) / 2; 320 | if (xCrd > mid) { 321 | return right; 322 | } else { 323 | return left; 324 | } 325 | } 326 | 327 | ///返回给定点是否在给定尺寸的修正范围内 328 | BOOL DWRectFixContainsPoint(CGRect rect,CGPoint point) { 329 | rect = CGRectInset(rect, 0, -0.25); 330 | return CGRectContainsPoint(rect, point); 331 | } 332 | 333 | ///比较指定坐标在给定尺寸中的位置 334 | NSComparisonResult DWCompareXCrdWithRect(CGFloat xCrd,CGRect rect) { 335 | CGFloat min = CGRectGetMinX(rect); 336 | CGFloat max = CGRectGetMaxX(rect); 337 | return DWNumBetweenAB(xCrd, min, max); 338 | } 339 | 340 | 341 | #pragma mark --- 尺寸修正方法 --- 342 | ///修正尺寸至指定坐标 343 | CGRect DWFixRectToXCrd(CGRect rect,CGFloat xCrd,NSComparisonResult result,BOOL backward) { 344 | if (CGRectEqualToRect(rect, CGRectZero)) { 345 | return CGRectZero; 346 | } 347 | if (result == NSOrderedDescending) { 348 | rect.size.width = xCrd - rect.origin.x; 349 | } else if (result == NSOrderedAscending) { 350 | rect.size.width += rect.origin.x - xCrd; 351 | rect.origin.x = xCrd; 352 | } else if (backward) { 353 | rect.size.width += rect.origin.x - xCrd; 354 | rect.origin.x = xCrd; 355 | } else { 356 | rect.size.width = xCrd - rect.origin.x; 357 | } 358 | if (CGRectGetWidth(rect) <= 0) { 359 | return CGRectZero; 360 | } 361 | return rect; 362 | } 363 | 364 | ///缩短CGRect至指定坐标 365 | CGRect DWShortenRectToXCrd(CGRect rect,CGFloat xCrd,BOOL backward) { 366 | if (!backward && xCrd == CGRectGetMaxX(rect)) { 367 | return rect; 368 | } 369 | if (backward && xCrd == CGRectGetMinX(rect)) { 370 | return rect; 371 | } 372 | NSComparisonResult result = DWCompareXCrdWithRect(xCrd, rect); 373 | if (result == NSOrderedSame) { 374 | return DWFixRectToXCrd(rect, xCrd, result, backward); 375 | } else { 376 | return CGRectZero; 377 | } 378 | } 379 | 380 | ///延长尺寸至指定坐标 381 | CGRect DWLengthenRectToXCrd(CGRect rect,CGFloat xCrd) { 382 | BOOL backward = YES; 383 | NSComparisonResult result = DWCompareXCrdWithRect(xCrd, rect); 384 | if (result == NSOrderedSame) { 385 | if (xCrd != CGRectGetMaxX(rect) && xCrd != CGRectGetMinX(rect)) { 386 | return CGRectZero; 387 | } 388 | return rect; 389 | } else if (result == NSOrderedAscending) { 390 | backward = NO; 391 | } 392 | return DWFixRectToXCrd(rect, xCrd, result, backward); 393 | } 394 | 395 | NSComparisonResult DWComparePoint(CGPoint p1,CGPoint p2) { 396 | if (CGPointEqualToPoint(p1, p2)) { 397 | return NSOrderedSame; 398 | } 399 | if (p2.y > p1.y) { 400 | return NSOrderedAscending; 401 | } else if (p2.y == p1.y && p2.x > p1.x) { 402 | return NSOrderedAscending; 403 | } 404 | return NSOrderedDescending; 405 | } 406 | 407 | #pragma mark --- 尺寸组合方法 --- 408 | 409 | NSValue * DWValueFromRect(CGRect rect) { 410 | return [NSValue valueWithCGRect:rect]; 411 | } 412 | 413 | NSValue * DWRectValue(CGFloat x,CGFloat y,CGFloat w,CGFloat h) { 414 | return DWValueFromRect(CGRectMake(x, y, w, h)); 415 | } 416 | 417 | NSArray * DWRectsBeyondRect(CGRect target,CGRect origin) { 418 | if (!CGRectIntersectsRect(origin, target)) { 419 | return @[DWValueFromRect(target)]; 420 | } 421 | if (CGRectContainsRect(origin,target)) { 422 | return @[]; 423 | } 424 | if (CGRectContainsRect(target, origin)) { 425 | return nil; 426 | } 427 | CGRect intersectR = CGRectIntersection(target, origin); 428 | NSInteger section = 0; 429 | if (CGRectGetMinY(intersectR) == CGRectGetMinY(target)) {///上边 430 | section += 1; 431 | } 432 | if (CGRectGetMinX(intersectR) == CGRectGetMinX(target)) {///左边 433 | section += 2; 434 | } 435 | if (CGRectGetMaxY(intersectR) == CGRectGetMaxY(target)) {///下边 436 | section += 4; 437 | } 438 | if (CGRectGetMaxX(intersectR) == CGRectGetMaxX(target)) {///右边 439 | section += 8; 440 | } 441 | // ________________ ________________ ________________ _______________ 442 | // | 3 | 1 | 9 | | | | | | | | | | | | 443 | // |____|____|____| |____|____|____| |____| |____| | | | 444 | // | 2 | 0 | 8 | | 10 | | | 5 | | | 7 | | 445 | // |____|____|____| |______________| |____| |____| | | | 446 | // | 6 | 4 | 12 | | | | | | | | | | | | 447 | // |____|____|____| |____|____|____| |____|____|____| |________|____| 448 | // 449 | // 450 | // ________________ ________________ ________________ 451 | // | | | | | | | 452 | // |______________| | | | | 11 | 453 | // | | | | 13 | | | 454 | // | 14 | | | | |______________| 455 | // | | | | | | | 456 | // |______________| |____|_________| |______________| 457 | // 458 | NSMutableArray * arr = @[].mutableCopy; 459 | if (section == 1) { 460 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, CGRectGetMinX(intersectR) - CGRectGetMinX(target), target.size.height)]; 461 | [arr addObjectsFromArray:DWRectsBeyondRect(CGRectMake(intersectR.origin.x, intersectR.origin.y, CGRectGetMaxX(target) - CGRectGetMinX(intersectR), target.size.height), origin)]; 462 | } else if (section == 2) { 463 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, intersectR.origin.y - target.origin.y)]; 464 | [arr addObjectsFromArray:DWRectsBeyondRect(CGRectMake(intersectR.origin.x, intersectR.origin.y, target.size.width, CGRectGetMaxY(target) - intersectR.origin.y), origin)]; 465 | } else if (section == 3) { 466 | [arr addObject:DWRectValue(CGRectGetMaxX(intersectR), target.origin.y, target.size.width - intersectR.size.width, target.size.height)]; 467 | [arr addObject:DWRectValue(target.origin.x, CGRectGetMaxY(intersectR), intersectR.size.width, target.size.height - intersectR.size.height)]; 468 | } else if (section == 4) { 469 | [arr addObject:DWRectValue(CGRectGetMaxX(intersectR), target.origin.y, CGRectGetMaxX(target) - CGRectGetMaxX(intersectR), target.size.height)]; 470 | [arr addObjectsFromArray:DWRectsBeyondRect(CGRectMake(target.origin.x, target.origin.y, CGRectGetMaxX(intersectR) - target.origin.x, target.size.height), origin)]; 471 | } else if (section == 5) { 472 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, CGRectGetMinX(intersectR) - CGRectGetMinX(target), target.size.height)]; 473 | [arr addObject:DWRectValue(CGRectGetMaxX(intersectR), intersectR.origin.y, CGRectGetMaxX(target) - CGRectGetMaxX(intersectR), target.size.height)]; 474 | } else if (section == 6) { 475 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, target.size.height - intersectR.size.height)]; 476 | [arr addObject:DWRectValue(CGRectGetMaxX(intersectR), intersectR.origin.y, target.size.width - intersectR.size.width, intersectR.size.height)]; 477 | } else if (section == 7) { 478 | [arr addObject:DWRectValue(CGRectGetMaxX(intersectR), target.origin.y, target.size.width - intersectR.size.width, target.size.height)]; 479 | } else if (section == 8) { 480 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, intersectR.origin.y - target.origin.y)]; 481 | [arr addObjectsFromArray:DWRectsBeyondRect(CGRectMake(target.origin.x, intersectR.origin.y, target.size.width, CGRectGetMaxY(target) - intersectR.origin.y), origin)]; 482 | } else if (section == 9) { 483 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width - intersectR.size.width, intersectR.size.height)]; 484 | [arr addObject:DWRectValue(target.origin.x,CGRectGetMaxY(intersectR), target.size.width, target.size.height - intersectR.size.height)]; 485 | } else if (section == 10) { 486 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, CGRectGetMinY(intersectR) - CGRectGetMinY(target))]; 487 | [arr addObject:DWRectValue(intersectR.origin.x, CGRectGetMaxY(intersectR), target.size.width, CGRectGetMaxY(target) - CGRectGetMaxY(intersectR))]; 488 | } else if (section == 11) { 489 | [arr addObject:DWRectValue(target.origin.x, CGRectGetMaxY(intersectR), target.size.width, target.size.height - intersectR.size.height)]; 490 | } else if (section == 12) { 491 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, target.size.height - intersectR.size.height)]; 492 | [arr addObject:DWRectValue(target.origin.x, CGRectGetMaxY(target) - intersectR.size.height, target.size.width - intersectR.size.width, intersectR.size.height)]; 493 | } else if (section == 13) { 494 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width - intersectR.size.width, target.size.height)]; 495 | } else if (section == 14) { 496 | [arr addObject:DWRectValue(target.origin.x, target.origin.y, target.size.width, target.size.height - intersectR.size.height)]; 497 | } 498 | return arr.copy; 499 | } 500 | 501 | 502 | #pragma mark --- 交换对象方法 --- 503 | ///交换浮点数 504 | void DWSwapfAB(CGFloat *a,CGFloat *b) { 505 | CGFloat temp = *a; 506 | *a = *b; 507 | *b = temp; 508 | } 509 | 510 | ///交换对象 511 | void DWSwapoAB(id a,id b) { 512 | id temp = a; 513 | a = b; 514 | b = temp; 515 | } 516 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLayout.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/8/10. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | /** 10 | 布局类,统一管理布局信息 11 | 12 | version 1.0.0 13 | 提供基础方法,方便布局计算(当前省略号处会导致CTRunGetStringRange及CTLineGetOffsetForStringIndex两个函数计算错误,尚未找到原因,通过特征信息判断后予以修复,日后找到原因后应寻找更加适合的修正方式) 14 | 修复中英文计算高度问题 15 | 图片padding以修复,选中效果更新方案,现已完成 16 | */ 17 | 18 | #import 19 | #import 20 | #import "DWCoreTextCommon.h" 21 | 22 | @class DWCTRunWrapper; 23 | /** 24 | 字形包装类 25 | */ 26 | @interface DWGlyphWrapper : NSObject 27 | 28 | ///所属run 29 | @property (nonatomic ,weak ,readonly) DWCTRunWrapper * run; 30 | 31 | ///上一个字形 32 | @property (nonatomic ,weak ,readonly) DWGlyphWrapper * previousGlyph; 33 | 34 | ///下一个字形 35 | @property (nonatomic ,weak ,readonly) DWGlyphWrapper * nextGlyph; 36 | 37 | ///字形开始x坐标 38 | @property (nonatomic ,assign ,readonly) CGFloat startXCrd; 39 | 40 | ///字形结束x坐标 41 | @property (nonatomic ,assign ,readonly) CGFloat endXCrd; 42 | 43 | ///字形角标 44 | @property (nonatomic ,assign ,readonly) NSUInteger index; 45 | 46 | ///起始位置 47 | @property (nonatomic ,assign ,readonly) DWPosition startPosition; 48 | 49 | ///终止位置 50 | @property (nonatomic ,assign ,readonly) DWPosition endPosition; 51 | 52 | @end 53 | 54 | @class DWCTLineWrapper; 55 | /** 56 | CTRun包装类 57 | */ 58 | @interface DWCTRunWrapper : NSObject 59 | 60 | ///所属Line 61 | @property (nonatomic ,weak ,readonly) DWCTLineWrapper * line; 62 | 63 | ///对应CTRun 64 | @property (nonatomic ,assign ,readonly) CTRunRef ctRun; 65 | 66 | ///对应的系统尺寸 67 | @property (nonatomic ,assign ,readonly) CGRect runRect; 68 | 69 | ///对应的屏幕尺寸 70 | @property (nonatomic ,assign ,readonly) CGRect frame; 71 | 72 | ///上一个CTRun 73 | @property (nonatomic ,weak ,readonly) DWCTRunWrapper * previousRun; 74 | 75 | ///下一个CTRun 76 | @property (nonatomic ,weak ,readonly) DWCTRunWrapper * nextRun; 77 | 78 | ///对应的CTRun的属性 79 | @property (nonatomic ,strong ,readonly) NSDictionary * runAttributes; 80 | 81 | ///本run包含的所有字符集 82 | @property (nonatomic ,strong ,readonly) NSArray * glyphs; 83 | 84 | ///起始位置(包含) 85 | @property (nonatomic ,assign ,readonly) NSUInteger startIndex; 86 | 87 | ///结束位置(不包含) 88 | @property (nonatomic ,assign ,readonly) NSUInteger endIndex; 89 | 90 | ///是否是图片 91 | @property (nonatomic ,assign ,readonly) BOOL isImage; 92 | 93 | ///图片实际绘制尺寸 94 | @property (nonatomic ,assign ,readonly) CGRect imageRect; 95 | 96 | ///是否具有事件 97 | @property (nonatomic ,assign ,readonly) BOOL hasAction; 98 | 99 | ///具有响应事件的属性字典 100 | @property (nonatomic ,strong ,readonly) NSDictionary * activeAttributes; 101 | 102 | @end 103 | 104 | 105 | /** 106 | CTLine包装类 107 | */ 108 | @interface DWCTLineWrapper : NSObject 109 | 110 | ///对应CTLine 111 | @property (nonatomic ,assign ,readonly) CTLineRef ctLine; 112 | 113 | ///系统坐标系原点(若要使用需转换成屏幕坐标系原点) 114 | @property (nonatomic ,assign ,readonly) CGPoint lineOrigin; 115 | 116 | ///对应的系统尺寸 117 | @property (nonatomic ,assign ,readonly) CGRect lineRect; 118 | 119 | ///对应的屏幕尺寸 120 | @property (nonatomic ,assign ,readonly) CGRect frame; 121 | 122 | ///起始位置(包含) 123 | @property (nonatomic ,assign ,readonly) NSUInteger startIndex; 124 | 125 | ///结束位置(不包含) 126 | @property (nonatomic ,assign ,readonly) NSUInteger endIndex; 127 | 128 | ///上一行 129 | @property (nonatomic ,weak ,readonly) DWCTLineWrapper * previousLine; 130 | 131 | ///下一行 132 | @property (nonatomic ,weak ,readonly) DWCTLineWrapper * nextLine; 133 | 134 | ///行数 135 | @property (nonatomic ,assign ,readonly) NSUInteger row; 136 | 137 | ///本行包含的ctRun数组 138 | @property (nonatomic ,strong ,readonly) NSArray * runs; 139 | 140 | @end 141 | 142 | 143 | /** 144 | CoreText绘制布局计算类 145 | 146 | 注:此处Layout类仅负责处理由富文本直接绘制的元素。包含文字、链接及插入到字符串的图片。 147 | */ 148 | @interface DWCoreTextLayout : NSObject 149 | 150 | ///包含的CTLine数组 151 | @property (nonatomic ,strong ,readonly) NSArray * lines; 152 | 153 | ///绘制的最大位置 154 | @property (nonatomic ,assign ,readonly) NSUInteger maxLoc; 155 | 156 | ///具有响应事件的图片的配置数组(Layout仅处理插入图片的图片配置数组,对于Path绘制的不处理) 157 | @property (nonatomic ,strong ,readonly) NSArray * activeImageConfigs; 158 | 159 | @property (nonatomic ,assign ,readonly) NSRange maxRange; 160 | 161 | 162 | /** 163 | 生成布局计算类 164 | 165 | @param ctFrame 需要绘制的CTFrame 166 | @param height 需要绘制CTFrame对应的屏幕坐标与系统坐标转换高度(即控件尺寸,包含空白、缩进等) 167 | @param considerGlyphs 是否计算每个字形 168 | @return 返回对应的绘制layout类 169 | */ 170 | +(instancetype)layoutWithCTFrame:(CTFrameRef)ctFrame convertHeight:(CGFloat)height considerGlyphs:(BOOL)considerGlyphs; 171 | 172 | 173 | 174 | /** 175 | 自动处理具有响应事件的图片及文字 176 | 177 | @param customLinkRegex 自定制链接匹配正则 178 | @param autoCheckLink 是否自动检测链接 179 | */ 180 | -(void)handleActiveImageAndTextWithCustomLinkRegex:(NSString *)customLinkRegex autoCheckLink:(BOOL)autoCheckLink; 181 | 182 | 183 | /** 184 | 遍历绘制所需CTRun 185 | 186 | @param handler 遍历回调 187 | */ 188 | -(void)enumerateCTRunUsingBlock:(void(^)(DWCTRunWrapper * run,BOOL * stop))handler; 189 | 190 | /*** 191 | 返回CTLine、CTRun、Glyph、Position或x坐标 192 | 193 | 注: 194 | loc为对应角标 195 | point为屏幕坐标系内的点 196 | */ 197 | -(DWCTLineWrapper *)lineAtLocation:(NSUInteger)loc; 198 | -(DWCTLineWrapper *)lineAtPoint:(CGPoint)point; 199 | -(DWCTRunWrapper *)runAtLocation:(NSUInteger)loc; 200 | -(DWCTRunWrapper *)runAtPoint:(CGPoint)point; 201 | -(DWGlyphWrapper *)glyphAtLocation:(NSUInteger)loc; 202 | -(DWGlyphWrapper *)glyphAtPoint:(CGPoint)point; 203 | -(DWPosition)positionAtLocation:(NSUInteger)loc; 204 | -(DWPosition)positionAtPoint:(CGPoint)point; 205 | -(CGFloat)xCrdAtLocation:(NSUInteger)loc; 206 | -(CGFloat)xCrdAtPoint:(CGPoint)point; 207 | 208 | 209 | /** 210 | 返回两个角标或点之间被选中的矩形尺寸数组 211 | 212 | 注: 213 | loc为对应角标(locA应小于locB,包含locA,不包含locB) 214 | point为屏幕坐标系内的点 215 | range为将要选中的范围 216 | */ 217 | -(NSArray *)selectedRectsBetweenLocationA:(NSUInteger)locA andLocationB:(NSUInteger)locB; 218 | -(NSArray *)selectedRectsBetweenPointA:(CGPoint)pointA andPointB:(CGPoint)pointB; 219 | -(NSArray *)selectedRectsInRange:(NSRange)range; 220 | -(NSArray *)selectedAllRects; 221 | 222 | /** 223 | 获取给定Line两坐标之间的所有字形矩阵尺寸 224 | 225 | @param line 给定Line 226 | @param x1 获取的第一个坐标 227 | @param x2 获取的第二个坐标 228 | @return 返回两坐标之间的尺寸 229 | */ 230 | -(CGRect)rectInLine:(DWCTLineWrapper *)line fromX1:(CGFloat)x1 toX2:(CGFloat)x2; 231 | 232 | 233 | /** 234 | 获取给定Line某点之前或之后的所有字形矩阵尺寸数组 235 | 236 | loc 角标 237 | point 目标点 238 | backward 是否取点之后的所有字形 239 | @return 符合条件的字形矩阵尺寸数组 240 | */ 241 | -(NSArray *)rectInLineAtLocation:(NSUInteger)loc backword:(BOOL)backward; 242 | -(NSArray *)rectInLineAtPoint:(CGPoint)point backword:(BOOL)backward; 243 | 244 | 245 | /** 246 | 获取同一行中两个run之间在两个坐标之间的字形矩阵尺寸数组 247 | 248 | @param startLine 开始的line 249 | @param startXCrd 开始的横坐标 250 | @param endLine 结束的line 251 | @param endXCrd 结束的横坐标 252 | @return 符合条件的字形矩阵尺寸数组 253 | */ 254 | -(NSArray *)rectsInLayoutWithStartLine:(DWCTLineWrapper *)startLine startXCrd:(CGFloat)startXCrd endLine:(DWCTLineWrapper *)endLine endXCrd:(CGFloat)endXCrd; 255 | 256 | 257 | /** 258 | 获取指定Line所有字形矩阵尺寸数组 259 | 260 | @param line 目标CTLine 261 | @return 目标CTLine的所有字形矩阵尺寸数组 262 | */ 263 | -(NSArray *)rectsInLine:(DWCTLineWrapper *)line; 264 | 265 | 266 | /** 267 | 获取点的位置返回角标 268 | 269 | @param point 屏幕中的点 270 | @return 对应角标 271 | 272 | 注: 273 | 返回点对应字形的角标 274 | */ 275 | -(NSUInteger)locFromPoint:(CGPoint)point; 276 | 277 | 278 | /** 279 | 返回较近一侧的坐标 280 | 281 | @param point 屏幕中的点 282 | @return 对应坐标 283 | 284 | 注: 285 | 返回点所在较近一侧的角标 286 | */ 287 | -(NSUInteger)closestLocFromPoint:(CGPoint)point; 288 | 289 | @end 290 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextLayout.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/8/10. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWCoreTextLayout.h" 10 | #import "DWCoreTextLabelCalculator.h" 11 | 12 | @implementation DWGlyphWrapper 13 | 14 | -(instancetype)initWithIndex:(NSUInteger)index startXCrd:(CGFloat)startXCrd endXCrd:(CGFloat)endXCrd { 15 | if (self = [super init]) { 16 | _index = index; 17 | [self configStartXCrd:startXCrd endXCrd:endXCrd]; 18 | } 19 | return self; 20 | } 21 | 22 | -(void)configStartXCrd:(CGFloat)startXCrd endXCrd:(CGFloat)endXCrd { 23 | _startXCrd = startXCrd; 24 | _endXCrd = endXCrd; 25 | } 26 | 27 | -(void)configRun:(DWCTRunWrapper *)run { 28 | _run = run; 29 | } 30 | 31 | -(void)configPreviousGlyph:(DWGlyphWrapper *)preGlyph { 32 | _previousGlyph = preGlyph; 33 | [preGlyph configNextGlyph:self]; 34 | } 35 | 36 | -(void)configNextGlyph:(DWGlyphWrapper *)nextGlyph { 37 | _nextGlyph = nextGlyph; 38 | } 39 | 40 | -(void)configPositionWithBaseLineY:(CGFloat)baseLineY height:(CGFloat)height { 41 | _startPosition = DWMakePosition(baseLineY, _startXCrd, height,_index); 42 | _endPosition = DWMakePosition(baseLineY, _endXCrd, height,_index + 1); 43 | } 44 | 45 | -(NSString *)debugDescription { 46 | NSString * string = [NSString stringWithFormat:@"%@ {",[super description]]; 47 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tindex:\t%lu",(unsigned long)self.index]]; 48 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tstartXCrd:\t%.2f",self.startXCrd]]; 49 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tendXCrd:\t%.2f\n}",self.endXCrd]]; 50 | return string; 51 | } 52 | 53 | @end 54 | 55 | @interface DWCTLineWrapper () 56 | 57 | -(void)configFrame:(CGRect)frame; 58 | 59 | @end 60 | 61 | @implementation DWCTRunWrapper 62 | 63 | +(instancetype)createWrapperForCTRun:(CTRunRef)ctRun { 64 | DWCTRunWrapper * wrapper = [[DWCTRunWrapper alloc] initWithCTRun:ctRun]; 65 | return wrapper; 66 | } 67 | 68 | -(instancetype)initWithCTRun:(CTRunRef)ctRun { 69 | if (self = [super init]) { 70 | CFSAFESETVALUEA2B(ctRun, _ctRun) 71 | _runAttributes = (NSDictionary *)CTRunGetAttributes(ctRun); 72 | CFRange range = CTRunGetStringRange(_ctRun); 73 | _startIndex = range.location; 74 | _endIndex = range.location + range.length; 75 | } 76 | return self; 77 | } 78 | 79 | -(void)configWithCTFrame:(CTFrameRef)ctFrame ctLine:(CTLineRef)ctLine origin:(CGPoint)origin convertHeight:(CGFloat)height { 80 | _runRect = getCTRunBounds(ctFrame, ctLine, origin, _ctRun); 81 | _frame = convertRect(_runRect, height); 82 | } 83 | 84 | -(void)configLine:(DWCTLineWrapper *)line { 85 | _line = line; 86 | } 87 | 88 | -(void)configPreviousRun:(DWCTRunWrapper *)preRun { 89 | _previousRun = preRun; 90 | [preRun configNextRun:self]; 91 | [self fixRunOriginX]; 92 | } 93 | 94 | ///TODO:针对省略号时CTLineGetOffsetForStringIndex无法正确计算结果返回0进行修正,以及range计算错误需要修复 95 | -(void)fixRunOriginX { 96 | if (_previousRun && (CGRectGetMinX(self.frame) < CGRectGetMaxX(_previousRun.frame))) { 97 | CGRect frame = self.frame; 98 | frame.origin.x = CGRectGetMaxX(_previousRun.frame); 99 | _frame = frame; 100 | _startIndex += _previousRun.endIndex; 101 | _endIndex += _previousRun.endIndex; 102 | } 103 | } 104 | 105 | -(void)configNextRun:(DWCTRunWrapper *)nextRun { 106 | _nextRun = nextRun; 107 | } 108 | 109 | -(void)handleGlyphsWithCTFrame:(CTFrameRef)ctFrame CTLine:(CTLineRef)ctLine origin:(CGPoint)origin { 110 | NSUInteger count = CTRunGetGlyphCount(_ctRun); 111 | NSMutableArray * temp = @[].mutableCopy; 112 | DWGlyphWrapper * preGlyph = nil; 113 | CGFloat baseLineY = CGRectGetMaxY(_line.frame); 114 | CGFloat height = CGRectGetHeight(_line.frame); 115 | for (int i = 0; i < count; i ++) { 116 | CGFloat offset = getCTFramePahtXOffset(ctFrame); 117 | NSUInteger index = _startIndex + i; 118 | CGFloat startXCrd = origin.x + CTLineGetOffsetForStringIndex(ctLine, index, NULL) + offset; 119 | CGFloat endXCrd = origin.x + CTLineGetOffsetForStringIndex(ctLine, index + 1, NULL) + offset; 120 | 121 | ///TODO:修复省略号前后CTLineGetOffsetForStringIndex计算不正确影响(尚未找到原因,只能手动修复) 122 | if (startXCrd < CGRectGetMinX(_frame)) { 123 | if (i == 0) { 124 | startXCrd = CGRectGetMinX(_frame); 125 | } 126 | } 127 | if (endXCrd < startXCrd || endXCrd > CGRectGetMaxX(_frame)) { 128 | endXCrd = CGRectGetMaxX(_frame); 129 | if (endXCrd < startXCrd) { 130 | startXCrd = endXCrd; 131 | } 132 | } 133 | 134 | DWGlyphWrapper * wrapper = [[DWGlyphWrapper alloc] initWithIndex:index startXCrd:startXCrd endXCrd:endXCrd]; 135 | [wrapper configRun:self]; 136 | [wrapper configPreviousGlyph:preGlyph]; 137 | [wrapper configPositionWithBaseLineY:baseLineY height:height]; 138 | preGlyph = wrapper; 139 | [temp addObject:wrapper]; 140 | } 141 | _glyphs = temp.copy; 142 | } 143 | 144 | -(void)handleActiveRunWithCustomLinkRegex:(NSString *)customLinkRegex autoCheckLink:(BOOL)autoCheckLink { 145 | CGRect deleteBounds = self.frame; 146 | _isImage = NO; 147 | _hasAction = NO; 148 | _activeAttributes = nil; 149 | if (CGRectEqualToRect(deleteBounds,CGRectNull)) {///无活动范围跳过 150 | return ; 151 | } 152 | NSDictionary * attributes = self.runAttributes; 153 | CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName]; 154 | NSMutableDictionary * dic = nil; 155 | if (delegate == nil) {///检测图片,不是图片检测文字 156 | dic = attributes[@"clickAttribute"]; 157 | if (!dic) {///不是活动文字检测自动链接及定制链接 158 | if (customLinkRegex.length) { 159 | dic = attributes[@"customLink"]; 160 | } 161 | 162 | if (!dic && autoCheckLink) { 163 | dic = attributes[@"autoCheckLink"]; 164 | } 165 | } 166 | _activeAttributes = dic; 167 | } else { 168 | dic = CTRunDelegateGetRefCon(delegate); 169 | if ([dic isKindOfClass:[NSMutableDictionary class]] && dic[@"image"]) { 170 | _isImage = YES; 171 | dic[@"drawPath"] = [UIBezierPath bezierPathWithRect:deleteBounds]; 172 | CGFloat padding = [dic[@"padding"] floatValue]; 173 | if (padding != 0) { 174 | deleteBounds = CGRectInset(deleteBounds, padding, 0); 175 | } 176 | _imageRect = deleteBounds; 177 | [_line configFrame:CGRectUnion(_line.frame, deleteBounds)]; 178 | if (_glyphs.count) { 179 | DWGlyphWrapper * g = self.glyphs.firstObject; 180 | [g configStartXCrd:CGRectGetMinX(deleteBounds) endXCrd:CGRectGetMaxX(deleteBounds)]; 181 | DWPosition p = g.startPosition; 182 | [g configPositionWithBaseLineY:p.baseLineY height:p.height]; 183 | } 184 | if (!CGRectEqualToRect(deleteBounds, CGRectZero)) { 185 | dic[@"frame"] = [NSValue valueWithCGRect:deleteBounds]; 186 | dic[@"activePath"] = [UIBezierPath bezierPathWithRect:deleteBounds]; 187 | } 188 | _activeAttributes = dic; 189 | } 190 | } 191 | if (_activeAttributes) { 192 | if (_activeAttributes[@"target"] && _activeAttributes[@"SEL"]) { 193 | _hasAction = YES; 194 | } 195 | } 196 | } 197 | 198 | -(NSString *)debugDescription { 199 | NSString * string = [NSString stringWithFormat:@"%@ {",[super description]]; 200 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tframe:\t%@",NSStringFromCGRect(self.frame)]]; 201 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tpreviousRun:\t%@",self.previousRun]]; 202 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tnextRun:\t%@\n}",self.nextRun]]; 203 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tstartIndex:\t%lu",(unsigned long)self.startIndex]]; 204 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tendIndex:\t%lu",(unsigned long)self.endIndex]]; 205 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tglyphs:\t%@\n}",self.glyphs]]; 206 | return string; 207 | } 208 | 209 | -(void)dealloc { 210 | CFSAFERELEASE(_ctRun) 211 | } 212 | 213 | @end 214 | 215 | @interface DWCTLineWrapper () 216 | 217 | @property (nonatomic ,strong) NSArray * totalLineRects; 218 | 219 | @end 220 | 221 | @implementation DWCTLineWrapper 222 | 223 | +(instancetype)createWrapperForCTLine:(CTLineRef)ctLine { 224 | DWCTLineWrapper * wrapper = [[DWCTLineWrapper alloc] initWithCTLine:ctLine]; 225 | return wrapper; 226 | } 227 | 228 | -(instancetype)initWithCTLine:(CTLineRef)ctLine { 229 | if (self = [super init]) { 230 | CFSAFESETVALUEA2B(ctLine, _ctLine) 231 | CFRange range = CTLineGetStringRange(ctLine); 232 | _startIndex = range.location; 233 | _endIndex = range.location + range.length; 234 | } 235 | return self; 236 | } 237 | 238 | -(void)configEndIndex:(NSUInteger)endIndex { 239 | _endIndex = endIndex; 240 | } 241 | 242 | -(void)configWithOrigin:(CGPoint)origin row:(NSUInteger)row ctFrame:(CTFrameRef)ctFrame convertHeight:(CGFloat)height { 243 | _lineOrigin = origin; 244 | _row = row; 245 | CGFloat lineAscent; 246 | CGFloat lineDescent; 247 | CGFloat lineWidth = CTLineGetTypographicBounds(_ctLine, &lineAscent, &lineDescent, nil); 248 | CGRect boundsLine = CGRectMake(0, - lineDescent, lineWidth, lineAscent + lineDescent); 249 | boundsLine = CGRectOffset(boundsLine, origin.x, origin.y); 250 | _lineRect = getRectWithCTFramePathOffset(boundsLine, ctFrame); 251 | [self configFrame:convertRect(_lineRect, height)]; 252 | } 253 | 254 | -(void)configFrame:(CGRect)frame { 255 | _frame = frame; 256 | } 257 | 258 | -(void)configCTRunsWithCTFrame:(CTFrameRef)ctFrame convertHeight:(CGFloat)height considerGlyphs:(BOOL)considerGlyphs { 259 | CFArrayRef runs = CTLineGetGlyphRuns(_ctLine); 260 | NSUInteger count = CFArrayGetCount(runs); 261 | NSMutableArray * runsA = @[].mutableCopy; 262 | DWCTRunWrapper * preRun = nil; 263 | for (int i = 0; i < count; i ++) { 264 | CTRunRef run = CFArrayGetValueAtIndex(runs, i); 265 | DWCTRunWrapper * runWrapper = [DWCTRunWrapper createWrapperForCTRun:run]; 266 | [runWrapper configWithCTFrame:ctFrame ctLine:_ctLine origin:_lineOrigin convertHeight:height]; 267 | [runWrapper configLine:self]; 268 | [runWrapper configPreviousRun:preRun]; 269 | if (considerGlyphs) { 270 | [runWrapper handleGlyphsWithCTFrame:ctFrame CTLine:_ctLine origin:_lineOrigin]; 271 | } 272 | preRun = runWrapper; 273 | [runsA addObject:runWrapper]; 274 | } 275 | _runs = runsA.copy; 276 | } 277 | 278 | -(void)configPreviousLine:(DWCTLineWrapper *)preLine { 279 | _previousLine = preLine; 280 | [preLine configNextLine:self]; 281 | } 282 | 283 | -(void)configNextLine:(DWCTLineWrapper *)nextLine { 284 | _nextLine = nextLine; 285 | } 286 | 287 | -(NSString *)debugDescription { 288 | NSString * string = [NSString stringWithFormat:@"%@ {",[super description]]; 289 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tframe:\t%@",NSStringFromCGRect(self.frame)]]; 290 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\trow:\t%lu",(unsigned long)self.row]]; 291 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\truns:\t%@",self.runs]]; 292 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tpreviousLine:\t%@",self.previousLine]]; 293 | string = [string stringByAppendingString:[NSString stringWithFormat:@"\n\tnextLine:\t%@\n}",self.nextLine]]; 294 | return string; 295 | } 296 | 297 | -(void)dealloc { 298 | CFSAFERELEASE(_ctLine) 299 | } 300 | 301 | @end 302 | 303 | @implementation DWCoreTextLayout 304 | +(instancetype)layoutWithCTFrame:(CTFrameRef)ctFrame convertHeight:(CGFloat)height considerGlyphs:(BOOL)considerGlyphs { 305 | DWCoreTextLayout * layout = [[DWCoreTextLayout alloc] initWithCTFrame:ctFrame convertHeight:height considerGlyphs:considerGlyphs]; 306 | return layout; 307 | } 308 | 309 | -(void)handleActiveImageAndTextWithCustomLinkRegex:(NSString *)customLinkRegex autoCheckLink:(BOOL)autoCheckLink { 310 | NSMutableArray * arr = @[].mutableCopy; 311 | [self enumerateCTRunUsingBlock:^(DWCTRunWrapper *run, BOOL *stop) { 312 | [run handleActiveRunWithCustomLinkRegex:customLinkRegex autoCheckLink:autoCheckLink]; 313 | if (run.isImage && run.activeAttributes[@"activePath"]) { 314 | [arr addObject:run.activeAttributes]; 315 | } 316 | }]; 317 | _activeImageConfigs = arr.copy; 318 | } 319 | 320 | -(void)enumerateCTRunUsingBlock:(void (^)(DWCTRunWrapper *, BOOL *))handler { 321 | if (!handler || self.lines.count == 0) { 322 | return; 323 | } 324 | 325 | BOOL stop = NO; 326 | for (int i = 0; i < self.lines.count; i ++) { 327 | DWCTLineWrapper * line = self.lines[i]; 328 | for (int j = 0; j < line.runs.count; j ++) { 329 | DWCTRunWrapper * runW = line.runs[j]; 330 | handler(runW,&stop); 331 | if (stop) { 332 | break; 333 | } 334 | } 335 | } 336 | } 337 | 338 | #pragma mark --- 以角标获取相关数据 --- 339 | -(DWCTLineWrapper *)lineAtLocation:(NSUInteger)loc { 340 | if (loc > _maxLoc) { 341 | return nil; 342 | } 343 | __block DWCTLineWrapper * line = nil; 344 | [self binarySearchInContainer:self.lines condition:^NSComparisonResult(DWCTLineWrapper * obj, NSUInteger currentIdx, BOOL *stop) { 345 | if (obj.startIndex <= loc && obj.endIndex > loc) { 346 | line = obj; 347 | return NSOrderedSame; 348 | } else if (obj.startIndex > loc) { 349 | return NSOrderedAscending; 350 | } else { 351 | return NSOrderedDescending; 352 | } 353 | }]; 354 | 355 | return line; 356 | } 357 | 358 | -(DWCTLineWrapper *)lineAtPoint:(CGPoint)point { 359 | __block DWCTLineWrapper * line = nil; 360 | [self binarySearchInContainer:self.lines condition:^NSComparisonResult(DWCTLineWrapper * obj, NSUInteger currentIdx, BOOL *stop) { 361 | if (DWRectFixContainsPoint(obj.frame, point)) { 362 | line = obj; 363 | return NSOrderedSame; 364 | } else { 365 | NSComparisonResult result = DWPointInRectV(point, obj.frame); 366 | if (result == NSOrderedSame) { 367 | return DWPointInRectH(point, obj.frame); 368 | } else { 369 | return result; 370 | } 371 | } 372 | }]; 373 | return line; 374 | } 375 | 376 | -(DWCTRunWrapper *)runAtLocation:(NSUInteger)loc { 377 | DWCTLineWrapper * line = [self lineAtLocation:loc]; 378 | if (!line) { 379 | return nil; 380 | } 381 | __block DWCTRunWrapper * run = nil; 382 | [self binarySearchInContainer:line.runs condition:^NSComparisonResult(DWCTRunWrapper * obj, NSUInteger currentIdx, BOOL *stop) { 383 | if (obj.startIndex <= loc && obj.endIndex > loc) { 384 | run = obj; 385 | return NSOrderedSame; 386 | } else if (obj.startIndex > loc) { 387 | return NSOrderedAscending; 388 | } else { 389 | return NSOrderedDescending; 390 | } 391 | }]; 392 | return run; 393 | } 394 | 395 | -(DWCTRunWrapper *)runAtPoint:(CGPoint)point { 396 | DWCTLineWrapper * line = [self lineAtPoint:point]; 397 | if (!line) { 398 | return nil; 399 | } 400 | __block DWCTRunWrapper * run = nil; 401 | [self binarySearchInContainer:line.runs condition:^NSComparisonResult(DWCTRunWrapper * obj, NSUInteger currentIdx, BOOL *stop) { 402 | if (DWRectFixContainsPoint(obj.frame, point)) { 403 | run = obj; 404 | return NSOrderedSame; 405 | } else { 406 | return DWNumBetweenAB(point.x, CGRectGetMinX(obj.frame), CGRectGetMaxX(obj.frame)); 407 | } 408 | }]; 409 | return run; 410 | } 411 | 412 | -(DWGlyphWrapper *)glyphAtLocation:(NSUInteger)loc { 413 | DWCTRunWrapper * run = [self runAtLocation:loc]; 414 | if (!run) { 415 | return nil; 416 | } 417 | NSUInteger idx = loc - run.startIndex; 418 | if (idx >= run.glyphs.count) { 419 | return nil; 420 | } 421 | return run.glyphs[idx]; 422 | } 423 | 424 | -(DWGlyphWrapper *)glyphAtPoint:(CGPoint)point { 425 | DWCTRunWrapper * run = [self runAtPoint:point]; 426 | if (!run) { 427 | return nil; 428 | } 429 | __block DWGlyphWrapper * glyph = nil; 430 | [self binarySearchInContainer:run.glyphs condition:^NSComparisonResult(DWGlyphWrapper * obj, NSUInteger currentIdx, BOOL *stop) { 431 | NSComparisonResult result = DWNumBetweenAB(point.x, obj.startXCrd, obj.endXCrd); 432 | if (result == NSOrderedSame) { 433 | glyph = obj; 434 | } 435 | return result; 436 | }]; 437 | return glyph; 438 | } 439 | 440 | -(DWPosition)positionAtLocation:(NSUInteger)loc { 441 | DWGlyphWrapper * glyph = [self glyphAtLocation:loc]; 442 | if (!glyph) { 443 | return DWPositionNull; 444 | } 445 | return glyph.startPosition; 446 | } 447 | 448 | -(DWPosition)positionAtPoint:(CGPoint)point { 449 | DWGlyphWrapper * glyph = [self glyphAtPoint:point]; 450 | if (!glyph) { 451 | return DWPositionNull; 452 | } 453 | CGFloat closestSide = DWClosestSide(point.x, glyph.startXCrd, glyph.endXCrd); 454 | if (closestSide == glyph.startXCrd) { 455 | return glyph.startPosition; 456 | } 457 | return glyph.endPosition; 458 | } 459 | 460 | -(CGFloat)xCrdAtLocation:(NSUInteger)loc { 461 | DWGlyphWrapper * glyph = [self glyphAtLocation:loc]; 462 | if (!glyph) { 463 | return MAXFLOAT; 464 | } 465 | return glyph.startXCrd; 466 | } 467 | 468 | -(CGFloat)xCrdAtPoint:(CGPoint)point { 469 | DWGlyphWrapper * glyph = [self glyphAtPoint:point]; 470 | if (!glyph) { 471 | return MAXFLOAT; 472 | } 473 | return DWClosestSide(point.x, glyph.startXCrd, glyph.endXCrd); 474 | } 475 | 476 | #pragma mark --- 获取指定范围内符合条件的字形矩阵尺寸数组 --- 477 | -(NSArray *)selectedRectsBetweenLocationA:(NSUInteger)locationA andLocationB:(NSUInteger)locationB { 478 | if (locationB > _maxLoc + 1) { 479 | return @[]; 480 | } 481 | if (locationA >= locationB) { 482 | return @[]; 483 | } 484 | locationB --;//函数出入的是不包含的locationB,所以自减至包含位置 485 | 486 | CGFloat startXCrd = [self xCrdAtLocation:locationA]; 487 | CGFloat endXCrd = [self glyphAtLocation:locationB].endXCrd; 488 | DWCTLineWrapper * startLine = [self lineAtLocation:locationA]; 489 | DWCTLineWrapper * endLine = [self lineAtLocation:locationB]; 490 | 491 | return [self rectsInLayoutWithStartLine:startLine startXCrd:startXCrd endLine:endLine endXCrd:endXCrd]; 492 | } 493 | 494 | -(NSArray *)selectedRectsBetweenPointA:(CGPoint)pointA andPointB:(CGPoint)pointB { 495 | DWCTLineWrapper * startLine = [self lineAtPoint:pointA]; 496 | DWCTLineWrapper * endLine = [self lineAtPoint:pointB]; 497 | if (startLine.startIndex > endLine.startIndex) {///保证小点在前 498 | DWSwapoAB(startLine, endLine); 499 | CGPoint temp = pointA; 500 | pointA = pointB; 501 | pointB = temp; 502 | } 503 | CGFloat startXCrd = [self xCrdAtPoint:pointA]; 504 | CGFloat endXCrd = [self xCrdAtPoint:pointB]; 505 | return [self rectsInLayoutWithStartLine:startLine startXCrd:startXCrd endLine:endLine endXCrd:endXCrd]; 506 | } 507 | 508 | -(NSArray *)selectedRectsInRange:(NSRange)range { 509 | return [self selectedRectsBetweenLocationA:range.location andLocationB:range.location + range.length]; 510 | } 511 | 512 | -(NSArray *)selectedAllRects { 513 | return [self selectedRectsBetweenLocationA:0 andLocationB:_maxLoc]; 514 | } 515 | 516 | ///返回run中对应的坐标之间的rect 517 | #pragma mark --- 获取给定Line两坐标之间的所有字形矩阵尺寸数组 --- 518 | -(CGRect)rectInLine:(DWCTLineWrapper *)line fromX1:(CGFloat)x1 toX2:(CGFloat)x2 { 519 | if (x1 == x2) { 520 | return CGRectZero; 521 | } 522 | if (x1 > x2) { 523 | DWSwapfAB(&x1, &x2); 524 | } 525 | CGRect rect = line.frame; 526 | if (x1 < CGRectGetMinX(rect)) { 527 | x1 = CGRectGetMinX(rect); 528 | } 529 | if (x2 > CGRectGetMaxX(rect)) { 530 | x2 = CGRectGetMaxX(rect); 531 | } 532 | rect = DWShortenRectToXCrd(rect, x1, YES); 533 | rect = DWShortenRectToXCrd(rect, x2, NO); 534 | return rect; 535 | } 536 | 537 | #pragma mark --- 获取给定Line某点之前或之后的所有字形矩阵尺寸数组 --- 538 | -(NSArray *)rectInLineAtLocation:(NSUInteger)loc backword:(BOOL)backward { 539 | if (loc > _maxLoc && backward) { 540 | return @[]; 541 | } 542 | if (!backward && loc > _maxLoc + 1) { 543 | return @[]; 544 | } 545 | DWCTLineWrapper * line = [self lineAtLocation:loc]; 546 | CGFloat xCrd = [self xCrdAtLocation:loc]; 547 | return [self rectsInLine:line xCrd:xCrd backward:backward]; 548 | } 549 | 550 | -(NSArray *)rectInLineAtPoint:(CGPoint)point backword:(BOOL)backward { 551 | DWCTLineWrapper * line = [self lineAtPoint:point]; 552 | CGFloat xCrd = [self xCrdAtPoint:point]; 553 | return [self rectsInLine:line xCrd:xCrd backward:backward]; 554 | } 555 | 556 | #pragma mark --- 获取指定CTLine所有字形矩阵尺寸数组 --- 557 | -(NSArray *)rectsInLine:(DWCTLineWrapper *)line { 558 | if (!line || !line.runs.count) { 559 | return @[]; 560 | } 561 | NSValue * v = [NSValue valueWithCGRect:line.frame]; 562 | return @[v]; 563 | } 564 | 565 | #pragma mark --- 返回任意两个Run直接介于起始终止坐标之间的所有字形矩阵尺寸数组 --- 566 | -(NSArray *)rectsInLayoutWithStartLine:(DWCTLineWrapper *)startLine startXCrd:(CGFloat)startXCrd endLine:(DWCTLineWrapper *)endLine endXCrd:(CGFloat)endXCrd { 567 | if (!startLine || !endLine || startXCrd == MAXFLOAT || endXCrd == MAXFLOAT) {///参数不合法 568 | return @[]; 569 | } 570 | if (startLine.startIndex > endLine.startIndex) {///参数不合法 571 | DWSwapoAB(startLine, endLine); 572 | } 573 | if ([startLine isEqual:endLine]) {///同一Line中 574 | CGRect r = [self rectInLine:startLine fromX1:startXCrd toX2:endXCrd]; 575 | return DWRectArray(r); 576 | } 577 | ///不同行 578 | NSMutableArray * rects = @[].mutableCopy; 579 | [rects addObjectsFromArray:[self rectsInLine:startLine xCrd:startXCrd backward:YES]]; 580 | while (![startLine.nextLine isEqual:endLine]) { 581 | startLine = startLine.nextLine; 582 | [rects addObjectsFromArray:[self rectsInLine:startLine]]; 583 | } 584 | [rects addObjectsFromArray:[self rectsInLine:endLine xCrd:endXCrd backward:NO]]; 585 | return rects; 586 | } 587 | 588 | #pragma mark --- 获取点的位置返回角标 --- 589 | -(NSUInteger)locFromPoint:(CGPoint)point { 590 | DWGlyphWrapper * glyph = [self glyphAtPoint:point]; 591 | if (!glyph) { 592 | return NSNotFound; 593 | } 594 | return glyph.index; 595 | } 596 | 597 | -(NSUInteger)closestLocFromPoint:(CGPoint)point { 598 | DWGlyphWrapper * glyph = [self glyphAtPoint:point]; 599 | if (!glyph) { 600 | return NSNotFound; 601 | } 602 | CGFloat xCrd = [self xCrdAtPoint:point]; 603 | if (xCrd == glyph.startXCrd) { 604 | return glyph.index; 605 | } else { 606 | return glyph.index + 1; 607 | } 608 | } 609 | 610 | #pragma mark --- 工具方法 --- 611 | -(instancetype)initWithCTFrame:(CTFrameRef)ctFrame convertHeight:(CGFloat)height considerGlyphs:(BOOL)considerGlyphs { 612 | if (self = [super init]) { 613 | CFArrayRef arrLines = CTFrameGetLines(ctFrame); 614 | NSUInteger count = CFArrayGetCount(arrLines); 615 | CGPoint points[count]; 616 | CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), points); 617 | DWCTLineWrapper * previousLine = nil; 618 | NSMutableArray * lineA = @[].mutableCopy; 619 | for (int i = 0; i < count; i++) { 620 | CTLineRef line = CFArrayGetValueAtIndex(arrLines, i); 621 | DWCTLineWrapper * lineWrap = [DWCTLineWrapper createWrapperForCTLine:line]; 622 | [lineWrap configWithOrigin:points[i] row:i ctFrame:ctFrame convertHeight:height]; 623 | [lineWrap configCTRunsWithCTFrame:ctFrame convertHeight:height considerGlyphs:considerGlyphs]; 624 | [lineWrap configPreviousLine:previousLine]; 625 | previousLine = lineWrap; 626 | [lineA addObject:lineWrap]; 627 | } 628 | _lines = lineA.copy; 629 | DWGlyphWrapper * lastGlyph = [self lastGlyphWrapper]; 630 | NSUInteger lastIndex = lastGlyph.index; 631 | [lastGlyph.run.line configEndIndex:lastIndex + 1]; 632 | _maxLoc = lastIndex; 633 | } 634 | return self; 635 | } 636 | 637 | -(DWGlyphWrapper *)lastGlyphWrapper { 638 | if (!_lines.count) { 639 | return nil; 640 | } 641 | DWGlyphWrapper * glyph = nil; 642 | DWCTLineWrapper * line = _lines.lastObject; 643 | do { 644 | if (!line.runs.count) { 645 | line = line.previousLine; 646 | continue; 647 | } 648 | DWCTRunWrapper * run = line.runs.lastObject; 649 | do { 650 | if (!run.glyphs.count) { 651 | run = run.previousRun; 652 | continue; 653 | } 654 | glyph = run.glyphs.lastObject; 655 | } while (run && !glyph); 656 | line = line.previousLine; 657 | } while (line && !glyph); 658 | return glyph; 659 | } 660 | 661 | /** 662 | 根据指定条件返回对应Line中xCrd及相应模式对应的字形矩阵尺寸数组 663 | 664 | @param line 指定位置的CTLine 665 | @param xCrd 指定位置的横坐标 666 | @param backward 是否为向后模式 667 | @return 符合条件的字形矩阵尺寸数组 668 | */ 669 | -(NSArray *)rectsInLine:(DWCTLineWrapper *)line xCrd:(CGFloat)xCrd backward:(BOOL)backward { 670 | if (!line || xCrd == MAXFLOAT) { 671 | return @[]; 672 | } 673 | CGFloat x2; 674 | if (backward) { 675 | x2 = line.runs.lastObject.glyphs.lastObject.endXCrd; 676 | } else { 677 | x2 = line.runs.firstObject.glyphs.firstObject.startXCrd; 678 | } 679 | CGRect r = [self rectInLine:line fromX1:xCrd toX2:x2]; 680 | return DWRectArray(r); 681 | } 682 | 683 | static inline NSArray * DWRectArray(CGRect r) { 684 | if (CGRectIsEmpty(r)) { 685 | return nil; 686 | } 687 | return @[[NSValue valueWithCGRect:r]]; 688 | } 689 | 690 | /** 691 | 二分法查找数组中指定元素 692 | 693 | @param container 需要查找的有序容器 694 | @param condition 对比条件 695 | - obj 当前找到元素 696 | - currentIdx 当前找到角标 697 | - *stop 是否停止查找 698 | 699 | eg. 在给定数组@[@1,@2,@3,@4,@5]中查找@2的角标 700 | 701 | NSArray * arr = @[@1,@2,@3,@4,@5]; 702 | __block NSUInteger idx = NSNotFound; 703 | [self binarySearchInContainer:arr condition:^NSComparisonResult(NSNumber * obj, NSUInteger currentIdx, BOOL *stop) { 704 | if ([obj isEqualToNumber:@2]) { 705 | idx = currentIdx; 706 | return NSOrderedSame; 707 | } else if (obj.integerValue > 2) { 708 | return NSOrderedAscending; 709 | } else { 710 | return NSOrderedDescending; 711 | } 712 | }]; 713 | if (idx == NSNotFound) { 714 | NSLog(@"未找到@2"); 715 | } else { 716 | NSLog(@"元素@2的角标为%lu",idx); 717 | } 718 | 719 | 注: 720 | 1.当condition为nil的时候会立刻返回 721 | 2.请确保被查找容器为有序容器 722 | 3.当查找到所需元素时,返回NSOrderedSame以停止查找过程 723 | 当未查找到所需元素时,返回NSOrderedAscending以检测角标较小的一侧 724 | 当未查找到所需元素时,返回NSOrderedDescending以检测角标较大的一侧 725 | 4.任何时候,可以通过修改stop为YES以停止查找过程 726 | */ 727 | -(void)binarySearchInContainer:(NSArray *)container condition:(NSComparisonResult(^)(id obj,NSUInteger currentIdx,BOOL * stop))condition { 728 | if (!condition || container.count == 0) { 729 | return; 730 | } 731 | NSUInteger hR = container.count - 1; 732 | NSUInteger lR = 0; 733 | NSUInteger mR = 0; 734 | BOOL stop = NO; 735 | while (lR <= hR) { 736 | mR = (hR + lR) / 2; 737 | NSComparisonResult result = condition(container[mR],mR,&stop); 738 | if (result == NSOrderedSame || stop == YES) { 739 | break; 740 | } else if (result == NSOrderedAscending) { 741 | if (mR == 0) { 742 | break; 743 | } else { 744 | hR = mR - 1; 745 | } 746 | } else { 747 | if (mR == container.count - 1) { 748 | break; 749 | } else { 750 | lR = mR + 1; 751 | } 752 | } 753 | } 754 | } 755 | 756 | #pragma mark --- setter/getter --- 757 | -(NSRange)maxRange { 758 | return NSMakeRange(0, _maxLoc + 1); 759 | } 760 | 761 | @end 762 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextSelectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextSelectionView.h 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/8/30. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | /** 10 | 选中状态视图 11 | 12 | 提供选中、插入状态的指示器及蒙版效果 13 | */ 14 | #import 15 | #import "DWCoreTextCommon.h" 16 | @class DWCoreTextLayout; 17 | @class DWCoreTextMenuItem; 18 | 19 | typedef NS_OPTIONS(NSUInteger, DWSelectAction) { 20 | DWSelectActionNone = 1 << 0,///无动作 21 | DWSelectActionCut = 1 << 1,///剪切 22 | DWSelectActionCopy = 1 << 2,///复制 23 | DWSelectActionPaste = 1 << 3,///粘贴 24 | DWSelectActionSelectAll = 1 << 4,///全选 25 | DWSelectActionDelete = 1 << 5,///删除 26 | DWSelectActionCustom = 1 << 6,///自定制 27 | }; 28 | 29 | 30 | /** 31 | 选中效果视图 32 | 33 | 提供选中效果及选中后的动作目录,提供插入指示器、拖动指示器 34 | */ 35 | @interface DWCoreTextSelectionView : UIView 36 | 37 | ///选中目录动作 38 | @property (nonatomic ,assign) DWSelectAction selectAction; 39 | 40 | ///自定义目录项 41 | @property (nonatomic ,strong) NSArray * customSelectItems; 42 | 43 | ///预置动作回调 44 | @property (nonatomic ,copy) void (^selectActionCallBack)(DWSelectAction action); 45 | 46 | /** 47 | 更新选中区域 48 | 49 | @param rects 选中区域尺寸数组 50 | 51 | @return 更新是否成功 52 | */ 53 | -(BOOL)updateSelectedRects:(NSArray *)rects; 54 | 55 | 56 | /** 57 | 更新两拖动指示器位置 58 | 59 | @param startP 起始范围拖动指示器位置 60 | @param endP 范围终止拖动指示器位置 61 | 62 | @return 更新是否成功 63 | */ 64 | -(BOOL)updateGrabberWithStartPosition:(DWPosition)startP endPosition:(DWPosition)endP; 65 | 66 | 67 | /** 68 | 更新选中区域 69 | 70 | @param rects 选中区域 71 | @param startP 起始范围拖动指示器位置 72 | @param endP 终止范围拖动指示器位置 73 | 74 | @return 更新是否成功 75 | */ 76 | -(BOOL)updateSelectedRects:(NSArray *)rects startGrabberPosition:(DWPosition)startP endGrabberPosition:(DWPosition)endP; 77 | 78 | 79 | /** 80 | 更新插入指示器位置 81 | 82 | @param position 位置 83 | 84 | @return 更新是否成功 85 | */ 86 | -(BOOL)updateCaretWithPosition:(DWPosition)position; 87 | 88 | 89 | /** 90 | 展示选择目录 91 | 92 | @param rect 要展示的位置 93 | */ 94 | -(void)showSelectMenuInRect:(CGRect)rect; 95 | 96 | 97 | /** 98 | 以当前选中区域自动展示选择目录 99 | */ 100 | -(void)showSelectMenu; 101 | 102 | 103 | /** 104 | 隐藏选择目录 105 | */ 106 | -(void)hideSelectMenu; 107 | 108 | @end 109 | 110 | 111 | /** 112 | 插入指示器视图 113 | */ 114 | @interface DWCoreTextCaretView : UIView 115 | 116 | ///是否闪烁 117 | @property (nonatomic ,assign ,getter=isBlinking) BOOL blinks; 118 | 119 | ///是否可见 120 | @property (nonatomic ,assign ,readonly ,getter=isVisible) BOOL visible; 121 | 122 | 123 | /** 124 | 生成插入指示器 125 | 126 | @param position 位置 127 | @return 插入指示器 128 | */ 129 | -(instancetype)initWithPosition:(DWPosition)position; 130 | 131 | ///显示插入指示器 132 | -(void)showCaret; 133 | 134 | ///隐藏插入指示器 135 | -(void)hideCaret; 136 | 137 | /** 138 | 保持底部坐标不变的情况下更新高度 139 | 140 | @param height 要更新的高度 141 | 142 | 注: 143 | 默认基线为下,所以保持底部坐标不变 144 | */ 145 | -(void)updateHeight:(CGFloat)height; 146 | 147 | 148 | /** 149 | 移动插入指示器 150 | 151 | @param baseLineY 基线纵坐标 152 | @param xCrd 指定位置横坐标 153 | */ 154 | -(void)moveToBaseLineY:(CGFloat)baseLineY xCrd:(CGFloat)xCrd; 155 | 156 | 157 | /** 158 | 设置插入指示器 159 | 160 | @param position 指定插入指示器的位置 161 | */ 162 | -(void)moveToPosition:(DWPosition)position; 163 | 164 | @end 165 | 166 | 167 | /** 168 | 选中范围拖动指示器 169 | */ 170 | @interface DWCoreTextGrabber : DWCoreTextCaretView 171 | 172 | @property (nonatomic ,assign) BOOL startGrabber; 173 | 174 | @end 175 | 176 | /** 177 | SelectView选中目录项 178 | 179 | 本类配合selectionView的customSelectItems属性使用。 180 | 由于UIMenuController自身实现导致,其所需动作实现须由其所对应添加的View实现。 181 | 由于SelectView中会将menu添加在自身上,而导致未实现对应的custom的动作导致崩溃。 182 | 为实现可扩展性custom属性有存在的必要性,故借助本类以及消息转发机制将消息转发至target对象,实现相应动作。 183 | customSelectItems数组中加入本类实例,selectionView会将对应消息转发至target。 184 | */ 185 | @interface DWCoreTextMenuItem : NSObject 186 | 187 | ///目录标题 188 | @property (nonatomic ,strong) NSString * title; 189 | 190 | ///目录动作 191 | @property (nonatomic ,assign) SEL action; 192 | 193 | ///目录动作target 194 | @property (nonatomic ,weak) id target; 195 | 196 | @end 197 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWCoreTextSelectionView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWCoreTextSelectionView.m 3 | // DWCoreTextLabel 4 | // 5 | // Created by Wicky on 2017/8/30. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWCoreTextSelectionView.h" 10 | 11 | @interface DWCoreTextSelectionView () 12 | 13 | @property (nonatomic ,strong) UIView * maskViewsContainer; 14 | 15 | @property (nonatomic ,strong) UIView * indicatorContainer; 16 | 17 | @property (nonatomic ,strong) DWCoreTextGrabber * startGrabber; 18 | 19 | @property (nonatomic ,strong) DWCoreTextGrabber * endGrabber; 20 | 21 | @property (nonatomic ,strong) DWCoreTextCaretView * caret; 22 | 23 | @property (nonatomic ,strong) NSArray * selArr; 24 | 25 | @property (nonatomic ,strong) NSMutableArray * selectedRects; 26 | 27 | @end 28 | 29 | @implementation DWCoreTextSelectionView 30 | 31 | -(BOOL)updateGrabberWithStartPosition:(DWPosition)startP endPosition:(DWPosition)endP { 32 | if (DWPositionIsNull(startP) || DWPositionIsNull(endP)) { 33 | return NO; 34 | } 35 | if (DWPositionIsZero(startP) || DWPositionIsZero(endP)) { 36 | [self.startGrabber hideCaret]; 37 | [self.endGrabber hideCaret]; 38 | return YES; 39 | } 40 | if (DWComparePosition(startP, endP) == NSOrderedDescending) { 41 | DWPosition temp = startP; 42 | startP = endP; 43 | endP = temp; 44 | } 45 | [self.caret hideCaret]; 46 | [self.startGrabber showCaret]; 47 | [self.endGrabber showCaret]; 48 | [self.startGrabber moveToPosition:startP]; 49 | [self.endGrabber moveToPosition:endP]; 50 | return YES; 51 | } 52 | 53 | -(BOOL)updateSelectedRects:(NSArray *)rects { 54 | [self hideSelectMenu]; 55 | [self.maskViewsContainer.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];///移除全部遮罩 56 | CGRect box = self.maskViewsContainer.bounds; 57 | __block BOOL updated = NO; 58 | NSMutableArray * selectedRects = @[].mutableCopy; 59 | [rects enumerateObjectsUsingBlock:^(NSValue * obj, NSUInteger idx, BOOL * _Nonnull stop) { 60 | CGRect rect = [obj CGRectValue]; 61 | if (CGRectIsEmpty(rect) || CGRectIsEmpty(CGRectIntersection(rect, box))) { 62 | return ; 63 | } 64 | if (!updated) { 65 | updated = YES; 66 | } 67 | [selectedRects addObject:obj]; 68 | UIView * mask = [[UIView alloc] initWithFrame:rect]; 69 | mask.backgroundColor = [UIColor colorWithRed:30 / 255.0 green:144 / 255.0 blue:1 alpha:0.3]; 70 | [self.maskViewsContainer addSubview:mask]; 71 | }]; 72 | if (updated) { 73 | self.selectedRects = selectedRects; 74 | } 75 | return updated; 76 | } 77 | 78 | -(BOOL)updateSelectedRects:(NSArray *)rects startGrabberPosition:(DWPosition)startP endGrabberPosition:(DWPosition)endP { 79 | BOOL updated = [self updateGrabberWithStartPosition:startP endPosition:endP]; 80 | if (updated) { 81 | updated |= [self updateSelectedRects:rects]; 82 | } 83 | return updated; 84 | } 85 | 86 | -(BOOL)updateCaretWithPosition:(DWPosition)position { 87 | if (DWPositionIsNull(position)) { 88 | return NO; 89 | } 90 | if (DWPositionIsZero(position)) { 91 | [self.caret hideCaret]; 92 | return YES;; 93 | } 94 | [self updateSelectedRects:@[]]; 95 | [self.startGrabber hideCaret]; 96 | [self.endGrabber hideCaret]; 97 | [self.caret showCaret]; 98 | [self.caret moveToPosition:position]; 99 | return YES; 100 | } 101 | 102 | -(void)showSelectMenu { 103 | __block CGRect rect = [self.selectedRects.firstObject CGRectValue]; 104 | [self.selectedRects enumerateObjectsUsingBlock:^(NSValue * obj, NSUInteger idx, BOOL * _Nonnull stop) { 105 | if (idx == 0) { 106 | return; 107 | } 108 | rect = CGRectUnion(rect, obj.CGRectValue); 109 | }]; 110 | [self showSelectMenuInRect:rect]; 111 | } 112 | 113 | -(void)hideSelectMenu { 114 | UIMenuController * menu = [UIMenuController sharedMenuController]; 115 | [menu setMenuVisible:NO]; 116 | } 117 | 118 | -(void)showSelectMenuInRect:(CGRect)rect { 119 | [self becomeFirstResponder]; 120 | UIMenuController * menu = [UIMenuController sharedMenuController]; 121 | NSMutableArray * actions = @[].mutableCopy; 122 | if (self.selectAction & DWSelectActionCopy) { 123 | [actions addObject:[[UIMenuItem alloc] initWithTitle:@"复制" action:@selector(dw_copy:)]]; 124 | } 125 | if (self.selectAction & DWSelectActionCut) { 126 | [actions addObject:[[UIMenuItem alloc] initWithTitle:@"剪切" action:@selector(dw_cut:)]]; 127 | } 128 | if (self.selectAction & DWSelectActionPaste) { 129 | [actions addObject:[[UIMenuItem alloc] initWithTitle:@"粘贴" action:@selector(dw_paste:)]]; 130 | } 131 | if (self.selectAction & DWSelectActionSelectAll) { 132 | [actions addObject:[[UIMenuItem alloc] initWithTitle:@"全选" action:@selector(dw_selectAll:)]]; 133 | } 134 | if (self.selectAction & DWSelectActionDelete) { 135 | [actions addObject:[[UIMenuItem alloc] initWithTitle:@"删除" action:@selector(dw_delete:)]]; 136 | } 137 | if (self.selectAction & DWSelectActionCustom) { 138 | 139 | if (self.customSelectItems.count) { 140 | __block NSMutableArray * temp = @[].mutableCopy; 141 | [self.customSelectItems enumerateObjectsUsingBlock:^(DWCoreTextMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 142 | [temp addObject:[[UIMenuItem alloc] initWithTitle:obj.title action:obj.action]]; 143 | }]; 144 | [actions addObjectsFromArray:temp]; 145 | } 146 | } 147 | menu.menuItems = actions; 148 | [menu setTargetRect:rect inView:self]; 149 | [menu setMenuVisible:YES animated:YES]; 150 | } 151 | 152 | -(NSArray *)customSelectActions { 153 | NSMutableArray * arr = @[].mutableCopy; 154 | return arr.copy; 155 | } 156 | 157 | -(void)setFrame:(CGRect)frame { 158 | [super setFrame:frame]; 159 | self.maskViewsContainer.frame = self.bounds; 160 | self.indicatorContainer.frame = self.bounds; 161 | } 162 | 163 | -(UIView *)maskViewsContainer { 164 | if (!_maskViewsContainer) { 165 | _maskViewsContainer = [[UIView alloc] initWithFrame:self.bounds]; 166 | [self addSubview:_maskViewsContainer]; 167 | } 168 | return _maskViewsContainer; 169 | } 170 | 171 | -(UIView *)indicatorContainer { 172 | if (!_indicatorContainer) { 173 | _indicatorContainer = [[UIView alloc] initWithFrame:self.bounds]; 174 | [self addSubview:_indicatorContainer]; 175 | } 176 | return _indicatorContainer; 177 | } 178 | 179 | -(BOOL)canBecomeFirstResponder { 180 | return YES; 181 | } 182 | 183 | -(BOOL)canPerformAction:(SEL)action withSender:(id)sender { 184 | NSString * actionStr = NSStringFromSelector(action); 185 | DWSelectAction actionO = DWSelectActionNone; 186 | if ([actionStr isEqualToString:@"dw_cut:"]) { 187 | actionO = DWSelectActionCut; 188 | } else if ([actionStr isEqualToString:@"dw_copy:"]) { 189 | actionO = DWSelectActionCopy; 190 | } else if ([actionStr isEqualToString:@"dw_paste:"]) { 191 | actionO = DWSelectActionPaste; 192 | } else if ([actionStr isEqualToString:@"dw_selectAll:"]) { 193 | actionO = DWSelectActionSelectAll; 194 | } else if ([actionStr isEqualToString:@"dw_delete:"]) { 195 | actionO = DWSelectActionDelete; 196 | } else { 197 | if ([self.selArr containsObject:actionStr]) { 198 | actionO = DWSelectActionCustom; 199 | } 200 | } 201 | return (self.selectAction & actionO) && !(actionO & DWSelectActionNone); 202 | } 203 | 204 | -(void)dw_cut:(UIMenuController *)menu { 205 | if (self.selectActionCallBack) { 206 | self.selectActionCallBack(DWSelectActionCut); 207 | } 208 | } 209 | 210 | -(void)dw_copy:(UIMenuController *)menu { 211 | if (self.selectActionCallBack) { 212 | self.selectActionCallBack(DWSelectActionCopy); 213 | } 214 | } 215 | 216 | -(void)dw_paste:(UIMenuController *)menu { 217 | if (self.selectActionCallBack) { 218 | self.selectActionCallBack(DWSelectActionPaste); 219 | } 220 | } 221 | 222 | -(void)dw_selectAll:(UIMenuController *)menu { 223 | if (self.selectActionCallBack) { 224 | self.selectActionCallBack(DWSelectActionSelectAll); 225 | } 226 | } 227 | 228 | -(void)dw_delete:(UIMenuController *)menu { 229 | if (self.selectActionCallBack) { 230 | self.selectActionCallBack(DWSelectActionDelete); 231 | } 232 | } 233 | 234 | -(id)forwardingTargetForSelector:(SEL)aSelector { 235 | __block id target = nil; 236 | [self.customSelectItems enumerateObjectsUsingBlock:^(DWCoreTextMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 237 | if (aSelector == obj.action) { 238 | target = obj.target; 239 | *stop = YES; 240 | } 241 | }]; 242 | return target; 243 | } 244 | 245 | -(NSArray *)selArr { 246 | NSMutableArray * arr = @[].mutableCopy; 247 | [self.customSelectItems enumerateObjectsUsingBlock:^(DWCoreTextMenuItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 248 | [arr addObject:NSStringFromSelector(obj.action)]; 249 | }]; 250 | return arr.copy; 251 | } 252 | 253 | -(DWCoreTextCaretView *)caret { 254 | if (!_caret) { 255 | _caret = [[DWCoreTextCaretView alloc] initWithPosition:DWPositionZero]; 256 | _caret.blinks = YES; 257 | [self.indicatorContainer addSubview:_caret]; 258 | } 259 | return _caret; 260 | } 261 | 262 | -(DWCoreTextGrabber *)startGrabber { 263 | if (!_startGrabber) { 264 | _startGrabber = [[DWCoreTextGrabber alloc] initWithPosition:DWPositionZero]; 265 | _startGrabber.startGrabber = YES; 266 | [self.indicatorContainer addSubview:_startGrabber]; 267 | } 268 | return _startGrabber; 269 | } 270 | 271 | -(DWCoreTextGrabber *)endGrabber { 272 | if (!_endGrabber) { 273 | _endGrabber = [[DWCoreTextGrabber alloc] initWithPosition:DWPositionZero]; 274 | [self.indicatorContainer addSubview:_endGrabber]; 275 | } 276 | return _endGrabber; 277 | } 278 | 279 | @end 280 | 281 | @implementation DWCoreTextCaretView 282 | 283 | -(instancetype)initWithPosition:(DWPosition)position { 284 | if (self = [super init]) { 285 | self.frame = CGRectFromPosition(position, 1); 286 | self.backgroundColor = [UIColor colorWithRed:30 / 255.0 green:144 / 255.0 blue:1 alpha:1]; 287 | self.layer.cornerRadius = 0.5; 288 | } 289 | return self; 290 | } 291 | 292 | -(void)setBlinks:(BOOL)blinks { 293 | if (blinks != _blinks) { 294 | _blinks = blinks; 295 | if (blinks) { 296 | if (self.isVisible) { 297 | [self.layer addAnimation:DWCoreTextCaretBlinkAnimation() forKey:@"blinkAnimation"]; 298 | } 299 | } else { 300 | [self.layer removeAnimationForKey:@"blinkAnimation"]; 301 | } 302 | } 303 | } 304 | 305 | -(void)showCaret { 306 | if (!self.isVisible) { 307 | self.hidden = NO; 308 | self.blinks = YES; 309 | } 310 | } 311 | 312 | -(void)hideCaret { 313 | if (self.isVisible) { 314 | self.blinks = NO; 315 | self.hidden = YES; 316 | } 317 | } 318 | 319 | -(void)updateHeight:(CGFloat)height { 320 | [self moveToPosition:DWMakePosition(CGRectGetMaxY(self.frame), CGRectGetMinX(self.frame), height,0)]; 321 | } 322 | 323 | -(void)moveToBaseLineY:(CGFloat)baseLineY xCrd:(CGFloat)xCrd { 324 | [self moveToPosition:DWMakePosition(baseLineY, xCrd, CGRectGetHeight(self.frame),0)]; 325 | } 326 | 327 | -(void)moveToPosition:(DWPosition)position { 328 | CGRect frame = self.frame; 329 | CGFloat oB = CGRectGetMaxY(frame); 330 | CGFloat oX = CGRectGetMinX(frame); 331 | CGFloat oH = CGRectGetHeight(frame); 332 | DWPosition oP = DWMakePosition(oB, oX, oH,0); 333 | if (!DWPositionEqualToPosition(position, oP)) { 334 | frame.origin.y = position.baseLineY - position.height; 335 | frame.origin.x = position.xCrd; 336 | frame.size.height = position.height; 337 | self.frame = frame; 338 | } 339 | } 340 | 341 | -(BOOL)isVisible { 342 | return !self.hidden && (self.alpha > 0); 343 | } 344 | 345 | CABasicAnimation * DWCoreTextCaretBlinkAnimation () { 346 | static CABasicAnimation * animation = nil; 347 | static dispatch_once_t onceToken; 348 | dispatch_once(&onceToken, ^{ 349 | animation = [CABasicAnimation animationWithKeyPath:@"hidden"]; 350 | animation.duration = 1.12; 351 | animation.fromValue = @1; 352 | animation.toValue = @0; 353 | animation.fillMode = kCAFillModeForwards; 354 | animation.repeatCount = MAXFLOAT; 355 | animation.removedOnCompletion = NO; 356 | }); 357 | return animation; 358 | } 359 | @end 360 | 361 | @interface DWCoreTextGrabber () 362 | 363 | @property (nonatomic ,strong) UIView * dot; 364 | 365 | @property (nonatomic ,assign) BOOL needsResetPot; 366 | 367 | @end 368 | 369 | @implementation DWCoreTextGrabber 370 | @dynamic blinks; 371 | 372 | -(instancetype)initWithPosition:(DWPosition)position { 373 | if (self = [super initWithPosition:position]) { 374 | _needsResetPot = YES; 375 | } 376 | return self; 377 | } 378 | 379 | -(void)moveToPosition:(DWPosition)position { 380 | [super moveToPosition:position]; 381 | _needsResetPot = YES; 382 | } 383 | 384 | -(void)layoutSubviews { 385 | [super layoutSubviews]; 386 | if (!self.dot) { 387 | self.dot = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; 388 | self.dot.backgroundColor = self.backgroundColor; 389 | self.dot.layer.cornerRadius = 5; 390 | [self addSubview:self.dot]; 391 | } 392 | if (_needsResetPot) { 393 | if (self.startGrabber) { 394 | self.dot.center = CGPointMake(1, 0); 395 | } else { 396 | self.dot.center = CGPointMake(0, self.bounds.size.height); 397 | } 398 | _needsResetPot = NO; 399 | } 400 | } 401 | 402 | -(void)setStartGrabber:(BOOL)startGrabber { 403 | if (_startGrabber != startGrabber) { 404 | _startGrabber = startGrabber; 405 | _needsResetPot = YES; 406 | } 407 | } 408 | 409 | -(void)setBlinks:(BOOL)blinks { 410 | ///不做动作 411 | } 412 | @end 413 | 414 | @implementation DWCoreTextMenuItem 415 | 416 | @end 417 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWWebImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // DWWebImage.h 3 | // DWAsyncImage 4 | // 5 | // Created by Wicky on 2017/2/4. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | /** 10 | DWWebImage 11 | 轻量级图片异步下载缓存类 12 | 13 | version 1.0.0 14 | 提供异步下载功能,提供图片缓存功能。 15 | 16 | version 1.0.1 17 | 添加断言 18 | 19 | version 1.0.2 20 | 下载完成后修改operation为finish状态 21 | */ 22 | 23 | 24 | #import 25 | 26 | #ifndef DWWebImageMarco 27 | #define DWWebImageMarco 28 | 29 | ///回调block 30 | typedef void(^DWWebImageCallBack)(UIImage * image); 31 | 32 | ///文件下载完成通知 33 | /** 34 | 下载成功userInfo携带图片url及image对象 35 | 下载失败携带错误原因 36 | */ 37 | #define DWWebImageDownloadFinishNotification @"DWWebImageDownloadFinishNotification" 38 | 39 | ///默认内存缓存成本(5Mb) 40 | #define DWWebImageCacheDefaultCost (5 * 1024 * 1024) 41 | 42 | ///默认磁盘缓存过期时间(7天) 43 | #define DWWebImageCacheDefaultExpirateTime (7 * 24 * 60 * 60) 44 | 45 | #endif 46 | 47 | typedef NS_OPTIONS(NSUInteger, DWWebImageCachePolicy) {///缓存策略 48 | DWWebImageCachePolicyNoCache = 1 << 0,///不缓存 49 | DWWebImageCachePolicyMemory = 1 << 1,///内存缓存 50 | DWWebImageCachePolicyDisk = 1 << 2,///磁盘缓存 51 | }; 52 | 53 | typedef NS_ENUM(NSUInteger, DWWebImageCacheType) {///缓存数据类型 54 | DWWebImageCacheTypeUndefined,///未定义 55 | DWWebImageCacheTypeData,///缓存data数据 56 | DWWebImageCacheTypeImage,///缓存image数据 57 | }; 58 | 59 | @interface UIButton (DWWebImage) 60 | 61 | -(void)dw_setImageWithUrl:(NSString *)url forState:(UIControlState)state; 62 | 63 | -(void)dw_setImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder forState:(UIControlState)state; 64 | 65 | -(void)dw_setBackgroundImageWithUrl:(NSString *)url forState:(UIControlState)state; 66 | 67 | -(void)dw_setBackgroundImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder forState:(UIControlState)state; 68 | 69 | @end 70 | 71 | @interface UIImageView (DWWebImage) 72 | 73 | -(void)dw_setImageWithUrl:(NSString *)url; 74 | 75 | -(void)dw_setImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder; 76 | 77 | @end 78 | 79 | #pragma mark --- 缓存管理类 --- 80 | @interface DWWebImageCache : NSObject 81 | 82 | ///缓存策略 83 | @property (nonatomic ,assign) DWWebImageCachePolicy cachePolicy; 84 | 85 | ///缓存数据类型 86 | @property (nonatomic ,assign) DWWebImageCacheType cacheType; 87 | 88 | ///缓存过期时间,默认值7天 89 | @property (nonatomic ,assign) unsigned long long expirateTime; 90 | 91 | ///是否加密缓存 92 | @property (nonatomic ,assign) BOOL useSecureKey; 93 | 94 | ///缓存空间 95 | @property (nonatomic ,copy) NSString * cacheSpace; 96 | 97 | ///单例 98 | +(instancetype)shareCache; 99 | 100 | ///通过key存缓存 101 | -(void)cacheObj:(id)obj forKey:(NSString *)key; 102 | 103 | ///通过key取缓存 104 | -(id)objCacheForKey:(NSString *)key; 105 | 106 | ///通过key移除缓存 107 | -(void)removeCacheByKey:(NSString *)key; 108 | 109 | ///移除过期缓存 110 | -(void)removeExpiratedCache; 111 | 112 | @end 113 | 114 | 115 | 116 | #pragma mark --- 图片下载类 --- 117 | @interface DWWebImageDownloader : NSObject 118 | 119 | ///回调数组 120 | @property (nonatomic ,strong) NSMutableArray * callBacks; 121 | 122 | ///下载任务 123 | @property (nonatomic ,strong) NSURLSessionDataTask * task; 124 | 125 | ///下载图像实例 126 | /** 127 | 任务完成前为nil 128 | */ 129 | @property (nonatomic ,strong) UIImage * image; 130 | 131 | ///现在完成标志 132 | @property (nonatomic ,assign) BOOL downloadFinish; 133 | 134 | ///初始化方法 135 | -(instancetype)initWithSession:(NSURLSession *)session; 136 | 137 | ///以url下载图片 138 | -(void)downloadImageWithUrlString:(NSString *)url; 139 | 140 | ///开启下载 141 | -(void)resume; 142 | 143 | ///取消下载 144 | -(void)cancel; 145 | 146 | @end 147 | 148 | 149 | 150 | #pragma mark --- 任务线程类 --- 151 | @interface DWWebImageOperation : NSOperation 152 | 153 | ///图片下载器 154 | @property (nonatomic ,strong) DWWebImageDownloader * donwloader; 155 | 156 | ///下载任务是否完成 157 | @property (nonatomic , assign, getter=isFinished) BOOL finished; 158 | 159 | ///以url及session下载图片 160 | -(instancetype)initWithUrl:(NSString *)url session:(NSURLSession *)session; 161 | 162 | @end 163 | 164 | 165 | 166 | #pragma mark --- 下载管理类 --- 167 | @interface DWWebImageManager : NSObject 168 | 169 | ///线程字典 170 | /** 171 | url为key,对应任务线程 172 | */ 173 | @property (nonatomic ,strong) NSMutableDictionary * operations; 174 | 175 | ///缓存管理对象 176 | @property (nonatomic ,strong) DWWebImageCache * cache; 177 | 178 | ///单例 179 | +(instancetype)shareManager; 180 | 181 | ///以url下载图片,进行回调 182 | -(void)downloadImageWithUrl:(NSString *)url completion:(DWWebImageCallBack)completion; 183 | 184 | ///以url移除下载任务 185 | -(void)removeOperationByUrl:(NSString *)url; 186 | 187 | @end 188 | -------------------------------------------------------------------------------- /DWCoreTextLabel/DWWebImage.m: -------------------------------------------------------------------------------- 1 | // 2 | // DWWebImage.m 3 | // DWAsyncImage 4 | // 5 | // Created by Wicky on 2017/2/4. 6 | // Copyright © 2017年 Wicky. All rights reserved. 7 | // 8 | 9 | #import "DWWebImage.h" 10 | #import 11 | 12 | #define dispatch_async_main_safe(block)\ 13 | if ([NSThread currentThread].isMainThread) {\ 14 | block();\ 15 | } else {\ 16 | dispatch_async(dispatch_get_main_queue(),block);\ 17 | } 18 | 19 | #define DWWebImageCacheCompleteNotification @"DWWebImageCacheCompleteNotification" 20 | #define DWErrorWithDescription(aCode,desc) [NSError errorWithDomain:@"com.Wicky.DWWebImage" code:aCode userInfo:@{NSLocalizedDescriptionKey:desc}] 21 | 22 | 23 | @implementation UIButton (DWWebImage) 24 | 25 | -(void)dw_setImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder forState:(UIControlState)state 26 | { 27 | if (placeHolder) { 28 | [self setImage:placeHolder forState:state]; 29 | } 30 | [[DWWebImageManager shareManager] downloadImageWithUrl:url completion:^(UIImage *image) { 31 | [self setImage:image forState:state]; 32 | }]; 33 | } 34 | 35 | -(void)dw_setImageWithUrl:(NSString *)url forState:(UIControlState)state 36 | { 37 | [self dw_setImageWithUrl:url placeHolder:nil forState:state]; 38 | } 39 | 40 | -(void)dw_setBackgroundImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder forState:(UIControlState)state 41 | { 42 | if (placeHolder) { 43 | [self setBackgroundImage:placeHolder forState:state]; 44 | } 45 | [[DWWebImageManager shareManager] downloadImageWithUrl:url completion:^(UIImage *image) { 46 | [self setBackgroundImage:image forState:state]; 47 | }]; 48 | } 49 | 50 | -(void)dw_setBackgroundImageWithUrl:(NSString *)url forState:(UIControlState)state 51 | { 52 | [self dw_setBackgroundImageWithUrl:url placeHolder:nil forState:state]; 53 | } 54 | 55 | @end 56 | 57 | @implementation UIImageView (DWWebImage) 58 | 59 | -(void)dw_setImageWithUrl:(NSString *)url placeHolder:(UIImage *)placeHolder 60 | { 61 | if (placeHolder) { 62 | self.image = placeHolder; 63 | } 64 | [[DWWebImageManager shareManager] downloadImageWithUrl:url completion:^(UIImage *image) { 65 | self.image = image; 66 | }]; 67 | } 68 | 69 | -(void)dw_setImageWithUrl:(NSString *)url 70 | { 71 | [self dw_setImageWithUrl:url placeHolder:nil]; 72 | } 73 | 74 | @end 75 | 76 | #pragma mark --- DWWebImageManager --- 77 | 78 | @interface DWWebImageManager () 79 | 80 | @property (nonatomic ,strong) NSURLSession * session; 81 | 82 | @property (nonatomic ,strong) dispatch_semaphore_t semaphore; 83 | 84 | @property (nonatomic ,strong) NSOperationQueue * queue; 85 | 86 | @property (nonatomic ,strong) DWWebImageOperation * lastOperation; 87 | 88 | @end 89 | 90 | @implementation DWWebImageManager 91 | 92 | -(instancetype)init 93 | { 94 | self = [super init]; 95 | if (self) { 96 | self.semaphore = dispatch_semaphore_create(1); 97 | self.cache = [DWWebImageCache shareCache]; 98 | self.cache.cachePolicy = DWWebImageCachePolicyDisk | DWWebImageCachePolicyMemory; 99 | [self.cache removeExpiratedCache]; 100 | dispatch_async_main_safe(^(){ 101 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cacheCompleteFinishNotice:) name:DWWebImageCacheCompleteNotification object:nil]; 102 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadFinishNotice:) name:DWWebImageDownloadFinishNotification object:nil]; 103 | }); 104 | } 105 | return self; 106 | } 107 | 108 | ///下载图片 109 | -(void)downloadImageWithUrl:(NSString *)url completion:(DWWebImageCallBack)completion 110 | { 111 | NSAssert(url.length, @"url不能为空"); 112 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 113 | ///从缓存加载图片 114 | UIImage * image = [UIImage imageWithData:[self.cache objCacheForKey:url]]; 115 | if (image) { 116 | dispatch_async_main_safe(^(){ 117 | completion(image); 118 | }); 119 | } else {///无缓存 120 | dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); 121 | DWWebImageOperation * operation = self.operations[url];///取出下载任务 122 | if (!operation) {///无任务 123 | operation = [[DWWebImageOperation alloc] initWithUrl:url session:self.session]; 124 | self.operations[url] = operation; 125 | if (self.lastOperation) { 126 | [self.lastOperation addDependency:operation]; 127 | } 128 | [self.queue addOperation:operation]; 129 | self.lastOperation = operation; 130 | } 131 | if (!operation.donwloader.downloadFinish) { 132 | [operation.donwloader.callBacks addObject:[completion copy]]; 133 | } else { 134 | ///从缓存读取图片回调 135 | dispatch_async_main_safe(^(){ 136 | completion(operation.donwloader.image); 137 | }); 138 | } 139 | dispatch_semaphore_signal(self.semaphore); 140 | } 141 | }); 142 | } 143 | 144 | ///下载完成回调 145 | -(void)downloadFinishNotice:(NSNotification *)sender 146 | { 147 | NSError * error = sender.userInfo[@"error"]; 148 | if (error) {///移除任务 149 | [self removeOperationByUrl:sender.userInfo[@"url"]]; 150 | [self removeCacheByUrl:sender.userInfo[@"url"]]; 151 | } else { 152 | NSString * url = sender.userInfo[@"url"]; 153 | DWWebImageOperation * operation = self.operations[url];///取出下载任务 154 | operation.finished = YES; 155 | } 156 | } 157 | 158 | ///缓存完成通知回调 159 | -(void)cacheCompleteFinishNotice:(NSNotification *)sender 160 | { 161 | NSString * url = sender.userInfo[@"url"]; 162 | if (url.length) { 163 | [self removeOperationByUrl:sender.userInfo[@"url"]]; 164 | } 165 | } 166 | 167 | ///移除下载进程 168 | -(void)removeOperationByUrl:(NSString *)url 169 | { 170 | DWWebImageOperation * operation = self.operations[url]; 171 | [operation cancel]; 172 | [self.operations removeObjectForKey:url]; 173 | } 174 | 175 | ///移除缓存 176 | -(void)removeCacheByUrl:(NSString *)url 177 | { 178 | [self.cache removeCacheByKey:url]; 179 | } 180 | 181 | -(NSMutableDictionary *)operations 182 | { 183 | if (!_operations) { 184 | _operations = [NSMutableDictionary dictionary]; 185 | } 186 | return _operations; 187 | } 188 | 189 | -(NSURLSession *)session 190 | { 191 | if (!_session) { 192 | NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration]; 193 | config.timeoutIntervalForRequest = 15; 194 | _session = [NSURLSession sessionWithConfiguration:config]; 195 | } 196 | return _session; 197 | } 198 | 199 | -(NSOperationQueue *)queue 200 | { 201 | if (!_queue) { 202 | _queue = [[NSOperationQueue alloc] init]; 203 | _queue.maxConcurrentOperationCount = 6; 204 | } 205 | return _queue; 206 | } 207 | 208 | #pragma mark --- 单例 --- 209 | static DWWebImageManager * mgr = nil; 210 | +(instancetype)shareManager 211 | { 212 | static dispatch_once_t onceToken; 213 | dispatch_once(&onceToken, ^{ 214 | mgr = [[self alloc] init]; 215 | }); 216 | return mgr; 217 | } 218 | 219 | +(instancetype)allocWithZone:(struct _NSZone *)zone 220 | { 221 | static dispatch_once_t onceToken; 222 | dispatch_once(&onceToken, ^{ 223 | mgr = [super allocWithZone:zone]; 224 | }); 225 | return mgr; 226 | } 227 | 228 | -(id)copyWithZone:(NSZone *)zone 229 | { 230 | return mgr; 231 | } 232 | 233 | @end 234 | 235 | 236 | 237 | #pragma mark --- DWWebImageDownloader --- 238 | @interface DWWebImageDownloader () 239 | 240 | @property (nonatomic ,copy) NSString * url; 241 | 242 | @property (nonatomic ,strong) NSURLSession * session; 243 | 244 | @end 245 | 246 | @implementation DWWebImageDownloader 247 | 248 | #pragma mark --- 接口方法 --- 249 | -(instancetype)initWithSession:(NSURLSession *)session { 250 | self = [super init]; 251 | if (self) { 252 | _session = session; 253 | _downloadFinish = NO; 254 | } 255 | return self; 256 | } 257 | 258 | -(instancetype)initWithUrl:(NSString *)url session:(NSURLSession *)session 259 | { 260 | self = [self initWithSession:session]; 261 | if (self) { 262 | _url = url; 263 | } 264 | return self; 265 | } 266 | 267 | -(void)downloadImageWithUrlString:(NSString *)url 268 | { 269 | if (!url.length) { 270 | dispatch_async_main_safe((^(){ 271 | [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10001,@"url为空"),@"url":self.url}]; 272 | })); 273 | return; 274 | } 275 | [self downloadImageWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]]; 276 | } 277 | 278 | -(void)resume { 279 | [self.task resume]; 280 | } 281 | 282 | -(void)cancel { 283 | [self.task cancel]; 284 | } 285 | 286 | #pragma mark --- Tool Method --- 287 | -(void)downloadImageWithRequest:(NSURLRequest *)request 288 | { 289 | if (!request) { 290 | dispatch_async_main_safe((^(){ 291 | [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10002,@"无法生成request对象"),@"url":self.url}]; 292 | })); 293 | return; 294 | } 295 | 296 | self.url = request.URL.absoluteString; 297 | 298 | self.task = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { 299 | if (error) {///下载错误 300 | dispatch_async_main_safe((^(){ 301 | [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10003, @"任务取消或错误"),@"url":self.url}]; 302 | })); 303 | return ; 304 | } 305 | _session = nil; 306 | UIImage * image = [UIImage imageWithData:data]; 307 | self.downloadFinish = YES;///标志下载完成 308 | self.image = image; 309 | if (!image) { 310 | dispatch_async_main_safe((^(){ 311 | [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"error":DWErrorWithDescription(10000, ([NSString stringWithFormat:@"图片下载失败:%@",self.url])),@"url":self.url}]; 312 | })); 313 | return ; 314 | } 315 | //保存数据 316 | [[DWWebImageCache shareCache] cacheObj:data forKey:self.url]; 317 | 318 | ///并发遍历 319 | [self.callBacks enumerateObjectsWithOptions:(NSEnumerationConcurrent | NSEnumerationReverse) usingBlock:^(DWWebImageCallBack _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 320 | if (obj) { 321 | //图片回调 322 | dispatch_async_main_safe(^(){ 323 | obj(image); 324 | }); 325 | } 326 | }]; 327 | ///发送通知 328 | dispatch_async_main_safe((^(){ 329 | [[NSNotificationCenter defaultCenter] postNotificationName:DWWebImageDownloadFinishNotification object:nil userInfo:@{@"url":self.url,@"image":image}]; 330 | })); 331 | }]; 332 | } 333 | 334 | -(NSMutableArray *)callBacks 335 | { 336 | if (!_callBacks) { 337 | _callBacks = [NSMutableArray array]; 338 | } 339 | return _callBacks; 340 | } 341 | 342 | @end 343 | 344 | 345 | 346 | #pragma mark --- DWWebImageOperation --- 347 | @implementation DWWebImageOperation 348 | @synthesize finished = _finished; 349 | -(instancetype)initWithUrl:(NSString *)url session:(NSURLSession *)session 350 | { 351 | self = [super init]; 352 | if (self) { 353 | _donwloader = [[DWWebImageDownloader alloc] initWithUrl:url session:session]; 354 | [_donwloader downloadImageWithUrlString:url]; 355 | } 356 | return self; 357 | } 358 | 359 | -(void)start 360 | { 361 | [super start]; 362 | [self.donwloader resume]; 363 | } 364 | 365 | -(void)cancel 366 | { 367 | [super cancel]; 368 | [self.donwloader cancel]; 369 | } 370 | 371 | -(void)setFinished:(BOOL)finished { 372 | [self willChangeValueForKey:@"isFinished"]; 373 | _finished = finished; 374 | [self didChangeValueForKey:@"isFinished"]; 375 | } 376 | 377 | @end 378 | 379 | 380 | 381 | #pragma mark --- DWWebImageCache --- 382 | @interface DWWebImageCache () 383 | 384 | @property (nonatomic ,strong) NSCache * memCache; 385 | 386 | @property (nonatomic ,strong) dispatch_semaphore_t semaphore; 387 | 388 | @property (nonatomic ,strong) NSFileManager * fileMgr; 389 | 390 | @end 391 | 392 | @implementation DWWebImageCache 393 | 394 | #pragma mark --- 接口方法 --- 395 | -(instancetype)init 396 | { 397 | self = [super init]; 398 | if (self) { 399 | _memCache = [[NSCache alloc] init]; 400 | _memCache.totalCostLimit = DWWebImageCacheDefaultCost; 401 | _memCache.countLimit = 20; 402 | _expirateTime = DWWebImageCacheDefaultExpirateTime; 403 | _useSecureKey = YES; 404 | _cachePolicy = DWWebImageCachePolicyDisk; 405 | _cacheType = DWWebImageCacheTypeData; 406 | _semaphore = dispatch_semaphore_create(1); 407 | _fileMgr = [NSFileManager defaultManager]; 408 | [self createTempPath]; 409 | } 410 | return self; 411 | } 412 | 413 | -(void)cacheObj:(id)obj forKey:(NSString *)key 414 | { 415 | NSString * url = key; 416 | key = transferKey(key, self.useSecureKey); 417 | if (self.cachePolicy & DWWebImageCachePolicyDisk) {///磁盘缓存 418 | writeFileWithKey(obj, url, key, self.semaphore, self.fileMgr,self.cacheSpace); 419 | } 420 | if (self.cachePolicy & DWWebImageCachePolicyMemory) { 421 | ///做内存缓存 422 | [self.memCache setObject:obj forKey:key cost:costForObj(obj)]; 423 | } 424 | } 425 | 426 | -(id)objCacheForKey:(NSString *)key 427 | { 428 | __block id obj = nil; 429 | key = transferKey(key, self.useSecureKey); 430 | obj = [self.memCache objectForKey:key]; 431 | if (!obj) { 432 | NSAssert((self.cacheType != DWWebImageCacheTypeUndefined), @"you must set a cacheType but not DWWebImageCacheTypeUndefined"); 433 | readFileWithKey(key, self.cacheType, self.semaphore, self.cacheSpace,^(id object) { 434 | obj = object; 435 | }); 436 | } 437 | return obj; 438 | } 439 | 440 | -(void)removeCacheByKey:(NSString *)key 441 | { 442 | key = transferKey(key, self.useSecureKey); 443 | [self.memCache removeObjectForKey:key]; 444 | [self.fileMgr removeItemAtPath:objPathWithKey(key,self.cacheSpace) error:nil]; 445 | } 446 | 447 | -(void)removeExpiratedCache 448 | { 449 | if (self.expirateTime) { 450 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 451 | NSDirectoryEnumerator *dir=[self.fileMgr enumeratorAtPath:sandBoxPath(self.cacheSpace)]; 452 | NSString *path=[NSString new]; 453 | unsigned long long timeStamp = [[NSDate date] timeIntervalSince1970]; 454 | while ((path=[dir nextObject])!=nil) { 455 | NSString * fileP = objPathWithKey(path,self.cacheSpace); 456 | NSDictionary * attrs = [self.fileMgr attributesOfItemAtPath:fileP error:nil]; 457 | NSDate * dataCreate = attrs[NSFileModificationDate]; 458 | if ((timeStamp - [dataCreate timeIntervalSince1970]) > self.expirateTime) { 459 | [self.fileMgr removeItemAtPath:fileP error:nil]; 460 | } 461 | } 462 | }); 463 | } 464 | } 465 | 466 | #pragma mark -- Tool Method --- 467 | -(void)createTempPath 468 | { 469 | if (![self.fileMgr fileExistsAtPath:sandBoxPath(self.cacheSpace)]) { 470 | [self.fileMgr createDirectoryAtPath:sandBoxPath(self.cacheSpace) withIntermediateDirectories:YES attributes:nil error:NULL]; 471 | } 472 | } 473 | 474 | #pragma mark --- Setter、getter --- 475 | -(void)setExpirateTime:(unsigned long long)expirateTime 476 | { 477 | _expirateTime = expirateTime; 478 | if (expirateTime) { 479 | [self removeExpiratedCache]; 480 | } 481 | } 482 | 483 | -(NSString *)cacheSpace 484 | { 485 | if (!_cacheSpace) { 486 | return @"defaultCacheSpace"; 487 | } 488 | return _cacheSpace; 489 | } 490 | 491 | #pragma mark --- 单例 --- 492 | static DWWebImageCache * cache = nil; 493 | +(instancetype)shareCache 494 | { 495 | static dispatch_once_t onceToken; 496 | dispatch_once(&onceToken, ^{ 497 | cache = [[self alloc] init]; 498 | }); 499 | return cache; 500 | } 501 | 502 | +(instancetype)allocWithZone:(struct _NSZone *)zone 503 | { 504 | static dispatch_once_t onceToken; 505 | dispatch_once(&onceToken, ^{ 506 | cache = [super allocWithZone:zone]; 507 | }); 508 | return cache; 509 | } 510 | 511 | -(id)copyWithZone:(NSZone *)zone 512 | { 513 | return cache; 514 | } 515 | 516 | #pragma mark --- 内联函数 --- 517 | 518 | /** 519 | 异步文件写入 520 | 521 | @param obj 写入对象 522 | @param url 下载url 523 | @param key 缓存key 524 | @param semaphore 信号量 525 | @param fileMgr 文件管理者 526 | @param cacheSpace 缓存空间 527 | */ 528 | static inline void writeFileWithKey(id obj,NSString * url,NSString * key,dispatch_semaphore_t semaphore,NSFileManager * fileMgr,NSString * cacheSpace){ 529 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 530 | dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 531 | NSString * path = objPathWithKey(key,cacheSpace); 532 | if ([fileMgr fileExistsAtPath:path]) { 533 | [fileMgr removeItemAtPath:path error:nil]; 534 | } 535 | if ([obj2Data(obj) writeToFile:path atomically:YES]) { 536 | dispatch_async_main_safe(^(){ 537 | [[NSNotificationCenter defaultCenter] postNotificationName: 538 | DWWebImageCacheCompleteNotification object:nil userInfo:@{@"url":url}]; 539 | }); 540 | } 541 | dispatch_semaphore_signal(semaphore); 542 | }); 543 | }; 544 | 545 | 546 | /** 547 | 文件读取 548 | 549 | @param key 缓存key 550 | @param type 文件类型 551 | @param semaphore 信号量 552 | @param cacheSpace 缓存空间 553 | @param completion 读取完成回调 554 | */ 555 | static inline void readFileWithKey(NSString * key,DWWebImageCacheType type,dispatch_semaphore_t semaphore,NSString * cacheSpace,void (^completion)(id obj)){ 556 | dispatch_sync(dispatch_get_global_queue(0, 0), ^{ 557 | dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 558 | NSData * data = [NSData dataWithContentsOfFile:objPathWithKey(key,cacheSpace)]; 559 | if (data && completion) { 560 | completion(transferDataToObj(data, type)); 561 | } 562 | dispatch_semaphore_signal(semaphore); 563 | }); 564 | }; 565 | 566 | 567 | /** 568 | 数据格式转换 569 | 570 | @param data 源数据 571 | @param type 数据类型 572 | @return 转换后数据 573 | */ 574 | static inline id transferDataToObj(NSData * data,DWWebImageCacheType type){ 575 | switch (type) { 576 | case DWWebImageCacheTypeData: 577 | return data; 578 | break; 579 | case DWWebImageCacheTypeImage: 580 | return [UIImage imageWithData:data]; 581 | break; 582 | default: 583 | return nil; 584 | break; 585 | } 586 | }; 587 | 588 | 589 | /** 590 | 返回文件路径 591 | 592 | @param key 缓存key 593 | @param cacheSpace 缓存空间 594 | @return 文件路径 595 | */ 596 | static inline NSString * objPathWithKey(NSString * key,NSString * cacheSpace){ 597 | return [NSString stringWithFormat:@"%@/%@",sandBoxPath(cacheSpace),key]; 598 | }; 599 | 600 | 601 | /** 602 | 对象转为NSData 603 | 604 | @param obj 对象 605 | @return 转换后data 606 | */ 607 | static inline NSData * obj2Data(id obj){ 608 | NSData * data = nil; 609 | if ([obj isKindOfClass:[NSData class]]) { 610 | data = obj; 611 | } 612 | else if([obj isKindOfClass:[UIImage class]]) { 613 | data = UIImageJPEGRepresentation(obj, 1); 614 | } 615 | return data; 616 | } 617 | 618 | 619 | /** 620 | 沙盒路径 621 | 622 | @param cacheSpace 缓存空间 623 | @return 沙盒路径 624 | */ 625 | static inline NSString * sandBoxPath(NSString * cacheSpace){ 626 | return [NSHomeDirectory() stringByAppendingString:[NSString stringWithFormat:@"/Documents/DWWebImageCache/%@/",cacheSpace]]; 627 | }; 628 | 629 | 630 | /** 631 | 计算对象所需缓存成本 632 | 633 | @param obj 对象 634 | @return 缓存成本 635 | */ 636 | static inline NSUInteger costForObj(id obj){ 637 | NSUInteger cost = 0; 638 | ///根据数据类型计算cost 639 | if ([obj isKindOfClass:[NSData class]]) { 640 | cost = [[obj valueForKey:@"length"] unsignedIntegerValue]; 641 | } else if ([obj isKindOfClass:[UIImage class]]) { 642 | UIImage * image = (UIImage *)obj; 643 | cost = (NSUInteger)image.size.width * image.size.height * image.scale * image.scale; 644 | } 645 | return cost; 646 | }; 647 | 648 | 649 | /** 650 | 返回缓存key 651 | 652 | @param originKey 原始key 653 | @param useSecureKey 是否加密 654 | @return 缓存key 655 | */ 656 | static inline NSString * transferKey(NSString * originKey,BOOL useSecureKey){ 657 | return useSecureKey?encryptToMD5(originKey):originKey; 658 | }; 659 | 660 | 661 | /** 662 | 返回MD5加密字符串 663 | 664 | @param str 原始字符串 665 | @return 加密后字符串 666 | */ 667 | static inline NSString *encryptToMD5(NSString * str){ 668 | CC_MD5_CTX md5; 669 | CC_MD5_Init (&md5); 670 | CC_MD5_Update (&md5, [str UTF8String], (CC_LONG)[str length]); 671 | 672 | unsigned char digest[CC_MD5_DIGEST_LENGTH]; 673 | CC_MD5_Final (digest, &md5); 674 | return [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", 675 | digest[0], digest[1], 676 | digest[2], digest[3], 677 | digest[4], digest[5], 678 | digest[6], digest[7], 679 | digest[8], digest[9], 680 | digest[10], digest[11], 681 | digest[12], digest[13], 682 | digest[14], digest[15]]; 683 | }; 684 | 685 | @end 686 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | DWCoreTextLabel 3 |

4 | 5 |

6 | DWCoreTextLabel 7 |

8 | 9 | ## Description 10 | This is an object which is based on CoreText and a subclass of UIView.It provides all dayly function of UILabel,and also enable collocation of illustration and character、click-action and other extense function at the same time. 11 | 12 | ## 描述 13 | 这是一个基于CoreText、继承自UIView的控件。它是一个可以满足UILabel所有日常功能并提供图文混排、点击事件等扩展功能的控件。 14 | 15 | ## Func 16 | - Insert image into a piece of text. 17 | - Draw image with text cover or around it. 18 | - Select a piece of text an copy it to pasteboard. 19 | - Add action to a piece of text or image. 20 | 21 | ## 功能 22 | - 向一段文本中插入图片。 23 | - 绘制图片并在其上绘制文字或者环绕着图片绘制文字。 24 | - 选中一段文字并复制到剪贴板。 25 | - 为一段文字或图片添加点击事件。 26 | 27 | ## Usage 28 | Firstly,drag it into your project or use cocoapods. 29 | 30 | pod 'DWCoreTextLabel' 31 | 32 | Then,to make you use it easily,I keep all the property the same as UILabel.So,use it as UILabel and other extense function you may understand its meaning by name. 33 | > And here are some API about image: 34 | > 35 | > To insert image by using the series API of dw_InsertImage... 36 | > 37 | > To draw image by using the series API of dw_DrawImage... 38 | > 39 | > To add click-action by using dw_AddTarget... 40 | > 41 | > To delete image by using dw_RemoveImageByID... 42 | 43 | ## 如何使用 44 | 首先,你应该将所需文件拖入工程中,或者你也可以用Cocoapods去集成他。 45 | 46 | pod 'DWCoreTextLabel' 47 | 48 | 为了让开发者能够更快上手,我保留了与UILabel相同的属性名。所以你可以像使用UILabel一样使用它,并且一些拓展属性也是见名知意的,看他的属性名你大概就该知道他的用途。 49 | 50 | > 还有一些图片相关的API: 51 | > 52 | > 使用dw_InsertImage...系列API向文字中插入图片。 53 | > 54 | > 使用dw_DrawImage...系列API在文字间绘制图片。 55 | > 56 | > 使用dw_AddTarget...API为文字或图片添加点击事件。 57 | > 58 | > 使用dw_RemoveImageByID...API为删除已经插入或绘制的图片。 59 | 60 | ## Contact With Me 61 | 62 | You may issue me on [my Github](https://github.com/CodeWicky/DWCoreTextLabel) or send me a email at [codeWicky@163.com]() to tell me some advices or the bug,I will be so appreciated. 63 | 64 | If you like it please give me a star. 65 | 66 | ## 联系作者 67 | 你可以通过在[我的Github](https://github.com/CodeWicky/DWCoreTextLabel)上给我留言或者给我发送电子邮件[codeWicky@163.com]()来给我提一些建议或者指出我的bug,我将不胜感激。 68 | 69 | 如果你喜欢这个小东西,记得给我一个star吧,么么哒~ 70 | 71 | ## Other 72 | 73 | If you want to learn something about CoreText,you may come to my Blog. 74 | 75 | [老司机的简书](http://www.jianshu.com/p/6db3289fb05d) 76 | 77 | ## 其他 78 | 如果你想学习一些CoreText的相关知识,你可以来老司机的博客 79 | 80 | [老司机的简书](http://www.jianshu.com/p/6db3289fb05d) 81 | -------------------------------------------------------------------------------- /动画展示.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWicky/DWCoreTextLabel/ee68b17907c7095d4cb0294294aad2cc6d8b186e/动画展示.gif --------------------------------------------------------------------------------