├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── AttributedTextView.playground ├── Contents.swift ├── contents.xcplayground ├── playground.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── timeline.xctimeline ├── AttributedTextView.podspec ├── AttributedTextView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── AttributedTextView-iOS.xcscheme │ ├── AttributedTextView-macOS.xcscheme │ ├── AttributedTextView-tvOS.xcscheme │ └── AttributedTextView-watchOS.xcscheme ├── AttributedTextView.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── Demo ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── AttributedLabelSubclassDemo.swift ├── AttributedTextViewSubclassDemo.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.swift ├── check.png ├── cross.png └── warning.png ├── LICENSE ├── Package.swift ├── Readme.md ├── Screenshots ├── IconList.png ├── Sample1.png ├── Sample2.png ├── Sample3.png ├── Sample4.png ├── Sample5.png └── Sample6.png ├── Sources ├── AttributedLabel.swift ├── AttributedTextView.h ├── AttributedTextView.swift ├── Attributer.swift ├── CGFloatDP.swift ├── Info-tvOS.plist ├── Info.plist ├── NSAttributedString+Html.swift ├── String+Attributer.swift └── String+NSRange.swift └── scripts └── bootstrap /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | Carthage/Build 33 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - variable_name 4 | - type_name 5 | - variable_name_min_length 6 | - type_body_length 7 | 8 | #- opening_brace 9 | #- comma 10 | #- colon 11 | #- control_statement 12 | #- statement_position 13 | 14 | #opt_in_rules: 15 | #- empty_count 16 | #- missing_docs 17 | 18 | 19 | excluded: 20 | - Pods/ 21 | - Sparen/Common/Generated/ 22 | - Sparen/Resources 23 | - GlobalAPIKey 24 | - _name 25 | 26 | force_cast: warning 27 | 28 | force_try: warning 29 | 30 | function_parameter_count: 31 | - 5 32 | - 12 33 | 34 | function_body_length: 35 | - 50 36 | - 120 37 | 38 | cyclomatic_complexity: 39 | - 20 40 | - 60 41 | 42 | line_length: 43 | - 200 44 | - 800 45 | 46 | type_body_length: 47 | - 500 48 | - 1000 49 | 50 | file_length: 51 | - 500 52 | - 2000 53 | 54 | type_name: 55 | min_length: 56 | warning: 5 57 | error: 2 58 | max_length: 59 | warning: 40 60 | error: 60 61 | 62 | variable_name: 63 | min_length: 64 | warning: 5 65 | error: 2 66 | max_length: 67 | warning: 80 68 | error: 50 69 | 70 | #reporter: "csv" # reporter type (xcode, json, csv, checkstyle) -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8 3 | 4 | env: 5 | global: 6 | - LC_CTYPE=en_US.UTF-8 7 | - LANG=en_US.UTF-8 8 | - WORKSPACE=EVReflection.xcworkspace 9 | - IOS_FRAMEWORK_SCHEME="EVReflection iOS" 10 | - MACOS_FRAMEWORK_SCHEME="EVReflection OSX" 11 | - TVOS_FRAMEWORK_SCHEME="EVReflection TVOS" 12 | - WATCHOS_FRAMEWORK_SCHEME="EVReflection WatchOS" 13 | - IOS_SDK=iphonesimulator10.0 14 | - MACOS_SDK=macosx10.12 15 | - TVOS_SDK=appletvsimulator10.0 16 | - WATCHOS_SDK=watchsimulator3.0 17 | 18 | matrix: 19 | - DESTINATION="OS=3.0,name=Apple Watch - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" SDK="$WATCHOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" 20 | - DESTINATION="OS=2.0,name=Apple Watch - 42mm" SCHEME="$WATCHOS_FRAMEWORK_SCHEME" SDK="$WATCHOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO" 21 | 22 | - DESTINATION="OS=10.0,name=iPhone 7 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="YES" 23 | - DESTINATION="OS=9.0,name=iPhone 5" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 24 | 25 | - DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" 26 | - DESTINATION="OS=9.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" 27 | 28 | - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" 29 | 30 | before_install: 31 | - gem install cocoapods --pre --no-rdoc --no-ri --no-document --quiet 32 | 33 | script: 34 | - set -o pipefail 35 | - xcodebuild -version 36 | - xcodebuild -showsdks 37 | 38 | # Build Framework in Debug and Run Tests if specified 39 | - if [ $RUN_TESTS == "YES" ]; then 40 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 41 | else 42 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty; 43 | fi 44 | 45 | # Build Framework in Release and Run Tests if specified 46 | - if [ $RUN_TESTS == "YES" ]; then 47 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 48 | else 49 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty; 50 | fi 51 | 52 | # Run `pod lib lint` if specified 53 | - if [ $POD_LINT == "YES" ]; then 54 | pod lib lint; 55 | fi 56 | 57 | -------------------------------------------------------------------------------- /AttributedTextView.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play. 2 | 3 | // Since interaction is not working, clicking the links won't do anything. 4 | // Please build the project first... First time rendering can take a minute 5 | 6 | 7 | import AttributedTextView 8 | import UIKit 9 | 10 | // In interfacebuilder put an UITextView on the canvas and set the base class to AttributedTextView. For here we just create a new instance. 11 | var textView1: AttributedTextView = AttributedTextView() 12 | 13 | // First basic test 14 | textView1.attributer = 15 | "1. ".red 16 | .append("This is the first test. ").green 17 | .append("Click on ").black 18 | .append("evict.nl").makeInteract { _ in 19 | UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in }) 20 | }.underline 21 | + " for testing links. ".black 22 | + "Next test".underline.makeInteract { _ in 23 | print("NEXT") 24 | } 25 | .all.font(UIFont(name: "SourceSansPro-Regular", size: 16)) 26 | .setLinkColor(UIColor.purple) // Does not work in playground 27 | var attributedText = textView1.attributedText // Vieuw the details for this --> 28 | 29 | // You can also use the Attributer for your UILabel. You only can't use the makeInteract function 30 | let myUILabel = UILabel() 31 | myUILabel.attributedText = ("Just ".red + "some ".green + "text.".orange).attributedText 32 | let showInPlayground = myUILabel.attributedText // Vieuw the details for this --> 33 | 34 | // Some more attributes and now using + instead of .append 35 | textView1.attributer = 36 | "2. red, ".red.underline.underline(0x00ff00) 37 | + "green, ".green.fontName("Helvetica").size(30) 38 | + "cyan, ".cyan.size(22) 39 | + "orange, ".orange.kern(10) 40 | + "blue, ".blue.strikethrough(3).baselineOffset(8) 41 | + "black.".shadow(color: UIColor.gray, offset: CGSize(width: 2, height: 3), blurRadius: 3.0) 42 | attributedText = textView1.attributedText // Vieuw the details for this --> 43 | 44 | // Match or matchAll 45 | textView1.attributer = "It is this or it is that where the word is is selected".size(20) 46 | .match("is").underline.underline(UIColor.red) 47 | .matchAll("is").strikethrough(4) 48 | attributedText = textView1.attributedText // Vieuw the details for this --> 49 | 50 | // Match or matchAny 51 | textView1.attributer = "The quick brown fox jumps over the lazy dog".size(20) 52 | .matchAny(["quick", "brown", "lazy"]).underline.underline(UIColor.red) 53 | attributedText = textView1.attributedText // Vieuw the details for this --> 54 | 55 | 56 | // Select hashtags or mentions 57 | textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library" 58 | .matchHashtags.underline 59 | .matchMentions 60 | .makeInteract { link in 61 | UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in }) 62 | } 63 | .setLinkColor(UIColor.red) // Does not work in playground 64 | attributedText = textView1.attributedText // Vieuw the details for this --> 65 | 66 | // Select links 67 | textView1.attributer = "link to http://evict.nl and https://github.com/evermeer" 68 | .matchLinks 69 | .makeInteract { link in 70 | UIApplication.shared.open(URL(string: link)!, options: [:], completionHandler: { completed in }) 71 | } 72 | attributedText = textView1.attributedText // View the details for this --> 73 | 74 | textView1.attributer = ( 75 | "test stroke".strokeWidth(2).strokeColor(UIColor.red).paragraphAlignCenter.paragraphApplyStyling 76 | + "test stroke\n".strokeWidth(2).strokeColor(UIColor.blue) 77 | + "test strikethrough".strikethrough(2).strikethroughColor(UIColor.red) 78 | + " test strikethrough 2\n".strikethrough(2).strikethroughColor(UIColor.yellow) 79 | + "expansion\n".expansion(0.8).paragraphAlignRight.paragraphApplyStyling 80 | + ("letterpress ".letterpress 81 | + " obliquenes\n".obliqueness(0.4).backgroundColor(UIColor.cyan)).paragraphAlignLeft.paragraphApplyStyling 82 | ).all.size(24) 83 | attributedText = textView1.attributedText // View the details for this --> 84 | 85 | textView1.attributer = ( 86 | "ligature fi and fl = ".ligature(0) + "fi and fl\n".ligature(1) 87 | + "0123456789".writingDirection([3]) 88 | ).all.fontName("Hoefler Text").size(24) 89 | attributedText = textView1.attributedText // View the details for this --> 90 | 91 | textView1.attributer = ( 92 | "The quick brown fox jumps over the lazy dog.\nPack my box with five dozen liquor jugs.\nSeveral fabulous dixieland jazz groups played with quick tempo.".paragraphLineHeightMultiple(5).paragraphLineSpacing(6).paragraphMinimumLineHeight(15).paragraphMaximumLineHeight(50).paragraphLineSpacing(10).paragraphLineBreakModeWordWrapping.paragraphFirstLineHeadIndent(20).paragraphApplyStyling 93 | ).all.size(12) 94 | attributedText = textView1.attributedText // View the details for this --> 95 | 96 | let rules = "Using backslash r has a strange effect on the last url if there is nothing behind it.\r\nhttp://serverurl.com/api.html\r\nhttp://serverurl.com/api2.html\r\nhttp://serverurl.com/api3.html" 97 | textView1.attributer = rules.matchLinks 98 | .makeInteract({ link in 99 | print("\(link)") 100 | }) 101 | attributedText = textView1.attributedText // View the details for this --> 102 | 103 | 104 | textView1.attributer = "And here we just do some tests with underline" 105 | .underline(.double, .patternDashDotDot) 106 | attributedText = textView1.attributedText // View the details for this --> 107 | 108 | 109 | 110 | textView1.attributer = "Edwin".red.fontName("Baskerville").size(15) 111 | .append("Edwin").gray.font(UIFont(name: "SourceSansPro-Regular", size: 15)) 112 | .append("Edwin").makeInteract({ _ in 113 | }).fontName("Helvetica").size(15) 114 | .setLinkColor(.orange) 115 | attributedText = textView1.attributedText // View the details for this --> 116 | 117 | 118 | 119 | textView1.attributer = "My name is: Edwin
With a bulet list
".html 120 | attributedText = textView1.attributedText // View the details for this --> 121 | -------------------------------------------------------------------------------- /AttributedTextView.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /AttributedTextView.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AttributedTextView.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AttributedTextView.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 59 | 60 | 64 | 65 | 69 | 70 | 74 | 75 | 79 | 80 | 84 | 85 | 89 | 90 | 94 | 95 | 99 | 100 | 104 | 105 | 109 | 110 | 114 | 115 | 119 | 120 | 124 | 125 | 130 | 131 | 135 | 136 | 140 | 141 | 145 | 146 | 150 | 151 | 155 | 156 | 160 | 161 | 165 | 166 | 170 | 171 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /AttributedTextView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AttributedTextView' 3 | s.version = '1.4.1' 4 | s.license = { :type => "MIT", :file => "LICENSE" } 5 | s.summary = 'Easiest way to create an attributed UILabel or UITextView (with support for multiple links and HTML)' 6 | s.homepage = 'https://github.com/evermeer/AttributedTextView' 7 | s.social_media_url = 'https://twitter.com/evermeer' 8 | s.authors = { "Edwin Vermeer" => "edwin@evict.nl" } 9 | s.source = { :git => "https://github.com/evermeer/AttributedTextView.git", :tag => "v"+s.version.to_s } 10 | s.platforms = { :ios => "8.0" } 11 | s.requires_arc = true 12 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '5.0' } 13 | s.swift_version = '5.0' 14 | s.swift_versions = ['4.0', '4.2', '5.0'] 15 | 16 | s.frameworks = "Foundation", "UIKit" 17 | s.source_files = "Sources/*.swift" 18 | end 19 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/xcshareddata/xcschemes/AttributedTextView-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/xcshareddata/xcschemes/AttributedTextView-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/xcshareddata/xcschemes/AttributedTextView-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /AttributedTextView.xcodeproj/xcshareddata/xcschemes/AttributedTextView-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /AttributedTextView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /AttributedTextView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Cartfile -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Cartfile.private -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v5.1.1" 2 | github "Quick/Quick" "v0.10.0" 3 | -------------------------------------------------------------------------------- /Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by Edwin Vermeer on 29/11/2016. 6 | // Copyright © 2016 evermeer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Demo/AttributedLabelSubclassDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedLabelSubclassDemo.swift 3 | // AttributedTextView 4 | // 5 | // Created by Edwin Vermeer on 03/03/2017. 6 | // Copyright © 2017 evermeer. All rights reserved. 7 | // 8 | 9 | import AttributedTextView 10 | import UIKit 11 | 12 | @IBDesignable open class AttributedLabelSubclassDemo: AttributedLabel { 13 | 14 | // Add these fields in interfacebuilder and make sure the interface is updated after changes 15 | 16 | // This field is used as the field seperator for the other fields 17 | @IBInspectable var seperator: String? { 18 | didSet { configureAttributedLabel() } 19 | } 20 | 21 | // Enter here the texts for all itmes seperated by the seperator field 22 | @IBInspectable var multiTexts: String? { 23 | didSet { configureAttributedLabel() } 24 | } 25 | 26 | // Enter here the words that will be highlighted seperated by the seperator field 27 | @IBInspectable var multiHighlights: String? { 28 | didSet { configureAttributedLabel() } 29 | } 30 | 31 | // Enter here all values for the icons. 0 = okImage, 1 = notOKImage, otherwise warningImage 32 | @IBInspectable var multiIconState: String? { 33 | didSet { configureAttributedLabel() } 34 | } 35 | 36 | // This image is used for icon state 1 37 | @IBInspectable var okImage: UIImage? { 38 | didSet { configureAttributedLabel() } 39 | } 40 | 41 | // This image is used for icon state 0 42 | @IBInspectable var notOkImage: UIImage? { 43 | didSet { configureAttributedLabel() } 44 | } 45 | 46 | // This image is used for icon states that are not 0 or 1 47 | @IBInspectable var warningImage: UIImage? { 48 | didSet { configureAttributedLabel() } 49 | } 50 | 51 | // Configure our custom styling. 52 | override open func configureAttributedLabel() { 53 | 54 | // First make sure all fields are ok and complete 55 | self.attributedText = Attributer("Incomplete settings.").attributedText 56 | self.numberOfLines = 0 57 | guard let seperator = self.seperator else { return } 58 | guard let texts: [String] = multiTexts?.components(separatedBy: seperator) else { return } 59 | guard let highlights: [String] = multiHighlights?.components(separatedBy: seperator) else { return } 60 | guard let states: [String] = multiIconState?.components(separatedBy: seperator) else { return } 61 | guard let okImage = self.okImage else { return } 62 | guard let notOkImage = self.notOkImage else { return } 63 | guard let warningImage = self.warningImage else { return } 64 | if texts.count != highlights.count || texts.count != states.count { return } 65 | 66 | // Start with an empty Attributer 67 | var attr = Attributer("") 68 | 69 | // The icon needs this bounds in order to position it correctly 70 | let rect = CGRect(x: 0, y: -4, width: 20, height: 20) 71 | 72 | // loop through the items and buildup the bulit list 73 | for (index, state) in states.enumerated() { 74 | 75 | // What image do you want for this item 76 | let image = (state == "1" ? okImage : (state == "0" ? notOkImage : warningImage)) 77 | 78 | // Build up a line and append it 79 | attr = attr.append( 80 | 81 | // The image 82 | Attributer(image, bounds: rect) 83 | 84 | // The text in green 85 | .append(" " + texts[index]).green.append("\n") 86 | 87 | // select the highlight text and make it red 88 | .match(highlights[index]).red 89 | ) 90 | } 91 | 92 | // apply the correct paragraph layout and set the attributedText 93 | self.attributedText = attr.all.paragraphHeadIndent(34) // Make sure a second line is alligned 94 | .paragraphLineSpacing(-2) // a little less than default spacing between lines 95 | .paragraphSpacing(15) // The space between items 96 | .paragraphApplyStyling.attributedText // apply the styling and put it in the attributedText 97 | 98 | // Just to be sure 99 | layoutIfNeeded() 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /Demo/AttributedTextViewSubclassDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedTextViewSubclassDemo.swift 3 | // AttributedTextView 4 | // 5 | // Created by Edwin Vermeer on 03/03/2017. 6 | // Copyright © 2017 evermeer. All rights reserved. 7 | // 8 | 9 | import AttributedTextView 10 | import UIKit 11 | 12 | @IBDesignable class AttributedTextViewSubclassDemo: AttributedTextView { 13 | 14 | // Add this field in interfacebuilder and make sure the interface is updated after changes 15 | @IBInspectable var linkText: String? { 16 | didSet { configureAttributedTextView() } 17 | } 18 | 19 | // Add this field in interfacebuilder and make sure the interface is updated after changes 20 | @IBInspectable var linkUrl: String? { 21 | didSet { configureAttributedTextView() } 22 | } 23 | 24 | // Configure our custom styling. 25 | override func configureAttributedTextView() { 26 | if let text = self.text, let linkText = self.linkText, let linkUrl = self.linkUrl { 27 | self.attributer = text.green.match(linkText).makeInteract { _ in 28 | if #available(iOS 10, *) { 29 | UIApplication.shared.open(URL(string: linkUrl)!, options: [:], completionHandler: { completed in }) 30 | } else { 31 | _ = UIApplication.shared.openURL(URL(string: linkUrl)!) 32 | } 33 | } 34 | } else { 35 | self.attributer = (self.text ?? "").green 36 | } 37 | layoutIfNeeded() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Demo/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/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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Demo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by Edwin Vermeer on 29/11/2016. 6 | // Copyright © 2016 evermeer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedTextView 11 | 12 | 13 | class ViewController: UIViewController { 14 | 15 | @IBOutlet weak var attributedTextView: AttributedTextView! 16 | 17 | // These are the samples for this demo 18 | lazy var samples: [(title: String, show: (()->()))] = [ 19 | ("custom link and attributes", showSample1), 20 | ("coloring and aditional attributes", showSample2), 21 | ("single or multiple matches", showSample3), 22 | ("hashtags and mentions", showSample4), 23 | ("creating your own composit style", showSample5), 24 | ("just some more styles", showSample6), 25 | ("paragraph formatting", showSample7), 26 | ("more paragraph formatting", showSample8) 27 | ] 28 | 29 | // For more basic tests about how to use AttributedTextView, see the playground 30 | 31 | override func viewDidAppear(_ animated: Bool) { 32 | showSample1() 33 | } 34 | 35 | // Dynamically build the header and the previous and next links around the content 36 | func decorate(_ id: Int, _ builder: ((_ content: Attributer) -> Attributer)) -> Attributer { 37 | var b = "Sample \(id + 1) of \(samples.count): \(samples[id].title)\n\n".red 38 | if id > 0 { 39 | b = b + "<-- previous sample\n\n".underline.makeInteract { _ in 40 | self.samples[id - 1].show() 41 | } 42 | } 43 | b = builder(b) // Now add the content 44 | if id < (samples.count - 1) { 45 | b = b + "\n\nnext sample -->".underline.makeInteract { _ in 46 | self.samples[id + 1].show() 47 | } 48 | } 49 | return b 50 | } 51 | 52 | // Basic test where we use the .append to paste the attributed string parts together 53 | func showSample1() { 54 | attributedTextView.attributer = decorate(0) { content in return content 55 | .append("This is the first test. ").green 56 | .append("Tap on ").black 57 | .append("evict.nl").makeInteract { _ in 58 | if #available(iOS 10, *) { 59 | UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { (completed) in }) 60 | } else { 61 | _ = UIApplication.shared.openURL(URL(string: "http://evict.nl")!) 62 | } 63 | }.underline 64 | .append(" for testing links. Or tap on the 'next sample' link below ").black 65 | .underline(.double, .patternDashDotDot) 66 | .append("[test]").makeInteract { text in 67 | print("makeInteract : \(text)") 68 | } 69 | } 70 | .all.font(UIFont(name: "SourceSansPro-Regular", size: 16)) // Font not availabel in this demo... 71 | .setLinkColor(UIColor.purple) // Will be set on the control so we also have to reset it when we show the next sample. 72 | } 73 | 74 | // Basic test where we use + to paste the attributed string parts together 75 | func showSample2() { 76 | attributedTextView.attributer = decorate(1) { content in return content 77 | + "green, ".green.fontName("Helvetica").size(30) 78 | + "cyan, ".cyan.size(22) 79 | + "orange, ".orange.kern(10) 80 | + "blue, ".blue.strikethrough(3).baselineOffset(8) 81 | + "black.".shadow(color: UIColor.gray, offset: CGSize(width: 2, height: 3), blurRadius: 3.0) 82 | } 83 | .setLinkColor(UIColor.blue) // Reset the link color to the default blue 84 | } 85 | 86 | // We can select 1 or more ranges in an attributed text and apply a style to that. 87 | func showSample3() { 88 | attributedTextView.attributer = decorate(2) { content in return content 89 | + "It is this or it is that where the word is is selected\n\n".size(20) 90 | .match("is").underline 91 | .underline(UIColor.red) 92 | .matchAll("is").strikethrough(4) 93 | + "Select any of the qords quick, brown and lazy in: The quick brown fox jumps over the lazy dog".size(16) 94 | .matchAny(["quick", "brown", "lazy"]).underline 95 | .underline(UIColor.red) 96 | } 97 | } 98 | 99 | // There are custom matchers for hashtags, metions, links or you can use a regexp with matchPattern 100 | func showSample4() { 101 | attributedTextView.attributer = decorate(3) { content in return content 102 | + "@test: What #hashtags do we have in @evermeer #AtributedTextView library" 103 | .matchHashtags.underline 104 | .makeInteract { link in 105 | if #available(iOS 10, *) { 106 | UIApplication.shared.open(URL(string: "https://twitter.com/hashtag/\(link.replacingOccurrences(of: "%23", with: ""))")!, options: [:], completionHandler: { completed in }) 107 | } else { 108 | _ = UIApplication.shared.openURL(URL(string: "https://twitter.com/hashtag/\(link.replacingOccurrences(of: "%23", with: ""))")!) 109 | } 110 | } 111 | .matchMentions 112 | .makeInteract { link in 113 | if #available(iOS 10, *) { 114 | UIApplication.shared.open(URL(string: "https://twitter.com/\(link.replacingOccurrences(of: "%40", with: ""))")!, options: [:], completionHandler: { completed in }) 115 | } else { 116 | _ = UIApplication.shared.openURL(URL(string: "https://twitter.com/\(link.replacingOccurrences(of: "%40", with: ""))")!) 117 | } 118 | } 119 | } 120 | } 121 | 122 | // Here we simply use our own composit style 123 | func showSample5() { 124 | attributedTextView.attributer = decorate(4) { content in return content 125 | + "This is our custom title".myTitle 126 | } 127 | } 128 | 129 | // just some other styles 130 | func showSample6() { 131 | attributedTextView.attributer = decorate(5) { content in return (content 132 | + ( "test stroke".strokeWidth(2).strokeColor(UIColor.red).paragraphAlignCenter.paragraphApplyStyling 133 | .append("test stroke\n").strokeWidth(2).strokeColor(UIColor.blue) 134 | .append("test strikethrough").strikethrough(2).strikethroughColor(UIColor.red) 135 | .append(" test strikethrough\n").strikethrough(2).strikethroughColor(UIColor.yellow) 136 | + "expansion\n".expansion(0.8).paragraphAlignRight.paragraphApplyStyling 137 | + ("letterpress ".letterpress 138 | + " obliquenes\n".obliqueness(0.4).backgroundColor(UIColor.cyan)).paragraphAlignRight.paragraphApplyStyling 139 | ).all.size(24) 140 | ) 141 | } 142 | } 143 | 144 | func showSample7() { 145 | attributedTextView.attributer = decorate(6) { content in return (content 146 | + ( "The quick brown fox jumps over the lazy dog.\nPack my box with five dozen liquor jugs.\nSeveral fabulous dixieland jazz groups played with quick tempo.".paragraphLineHeightMultiple(2).paragraphLineSpacing(5).paragraphMinimumLineHeight(15).paragraphMaximumLineHeight(30).paragraphLineSpacing(10).paragraphLineBreakModeWordWrapping.paragraphFirstLineHeadIndent(20).paragraphApplyStyling 147 | ).all.size(12) 148 | ) 149 | } 150 | } 151 | 152 | func showSample8() { 153 | attributedTextView.attributer = decorate(7) { content in return (content + ( 154 | "The quick brown fox jumps over the lazy dog.\nPack my box with five dozen liquor jugs.\nSeveral fabulous dixieland jazz groups played with quick tempo.".brown 155 | .match("brown fox").underline.makeInteract { (link) in 156 | print("TODO: open terms of user screen") 157 | } 158 | .match("fabulous dixieland").underline.makeInteract { (link) in 159 | print("TODO: open privacy policy") 160 | }.all.paragraphAlignRight.paragraphApplyStyling 161 | )) 162 | } 163 | } 164 | 165 | } 166 | 167 | 168 | // Extending the Attributer and String so that it supports a custom composit style. See the showSample5 169 | 170 | extension Attributer { 171 | open var myTitle: Attributer { 172 | get { 173 | return self.fontName("Arial").size(28).color(0xffaa66).kern(5) 174 | } 175 | } 176 | } 177 | 178 | public extension String { 179 | var myTitle: Attributer { 180 | get { 181 | return attributer.myTitle 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /Demo/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Demo/check.png -------------------------------------------------------------------------------- /Demo/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Demo/cross.png -------------------------------------------------------------------------------- /Demo/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Demo/warning.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT 3 License 2 | 3 | Copyright (c) 2016, EVICT B.V. 4 | All rights reserved. 5 | http://evict.nl, mailto://edwin@evict.nl 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of EVICT B.V. nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // 3 | // AttributedTextView.swift 4 | // Restofire 5 | // 6 | // Created by Edwin Vermeer on 23/10/15. 7 | // Copyright © 2016 evermeer. All rights reserved. 8 | 9 | import PackageDescription 10 | 11 | let packageName = "AttributedTextView" 12 | let package = Package( 13 | name: packageName, 14 | platforms: [ 15 | .iOS(.v8), 16 | ], 17 | products: [ 18 | .library( 19 | name: packageName, 20 | targets: [packageName] 21 | ) 22 | ], 23 | dependencies: [ 24 | // Stub 25 | ], 26 | targets: [ 27 | .target( 28 | name: packageName, 29 | path: "Sources" 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## AttributedTextView 2 | 3 | [![Issues](https://img.shields.io/github/issues-raw/evermeer/AttributedTextView.svg?style=flat)](https://github.com/evermeer/AttributedTextView/issues) 4 | [![Documentation](https://img.shields.io/badge/documented-99%-green.svg?style=flat)](http://cocoadocs.org/docsets/AttributedTextView) 5 | [![Stars](https://img.shields.io/github/stars/evermeer/AttributedTextView.svg?style=flat)](https://github.com/evermeer/AttributedTextView/stargazers) 6 | 7 | [![Version](https://img.shields.io/cocoapods/v/AttributedTextView.svg?style=flat)](http://cocoadocs.org/docsets/AttributedTextView) 8 | [![Swift Package Manager](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | [![Language](https://img.shields.io/badge/language-swift%204-f48041.svg?style=flat)](https://developer.apple.com/swift) 11 | [![Platform](https://img.shields.io/cocoapods/p/AttributedTextView.svg?style=flat)](http://cocoadocs.org/docsets/AttributedTextView) 12 | [![License](https://img.shields.io/cocoapods/l/AttributedTextView.svg?style=flat)](http://cocoadocs.org/docsets/AttributedTextView) 13 | 14 | [![Git](https://img.shields.io/badge/GitHub-evermeer-blue.svg?style=flat)](https://github.com/evermeer) 15 | [![Twitter](https://img.shields.io/badge/twitter-@evermeer-blue.svg?style=flat)](http://twitter.com/evermeer) 16 | [![LinkedIn](https://img.shields.io/badge/linkedin-Edwin%20Vermeer-blue.svg?style=flat)](http://nl.linkedin.com/in/evermeer/en) 17 | [![Website](https://img.shields.io/badge/website-evict.nl-blue.svg?style=flat)](http://evict.nl) 18 | [![eMail](https://img.shields.io/badge/email-edwin@evict.nl-blue.svg?style=flat)](mailto:edwin@evict.nl?SUBJECT=About%20EVReflection) 19 | 20 | 21 | Easiest way to create an attributed UITextView (with support for multiple links and html). 22 | 23 | See the demo app and the playground for detailed information how to use AttributedTextView 24 | 25 | - [Requirements](#requirements) 26 | - [Usage](#usage) 27 | - [General usage](#general-usage) 28 | - [Paragraph styling](#paragraph-styling) 29 | - [The active range](#the-active-range) 30 | - [Sample code](#sample-code) 31 | - [Use the attributedtext functionality on a UILabel](#use-the-attributedtext-functionality-on-a-uilabel) 32 | - [Extending AttributedTextView](#extending-attributedtextview) 33 | - [Decorating the Attributed object](#decorating-the-attributed-object) 34 | - [Creating your own custom label](#creating-your-own-custom-label) 35 | - [Creating your own custom textview](#creating-your-own-custom-textview) 36 | - [Installation](#installation) 37 | - [License](#license) 38 | - [My other libraries](#My-other-libraries) 39 | 40 | ## Requirements 41 | 42 | - iOS 8.0+ 43 | - Xcode 8.0+ 44 | 45 | ## Usage 46 | 47 | ### General usage 48 | In interfacebuilder put an UITextView on the canvas and set the base class to AttributedTextView and create a referencing outlet to the a property in your viewController. In the samples below we have called this property textView1. Always assign to the attributer property when you want to set something. 49 | 50 | ### Paragraph styling 51 | You do have to be aware that the paragraph functions will only be applied after calling the .paragraphApplyStyling function. On start the paragraph styling will use default styling. After each range change (what happens after .all, .match* or .append) the styling will be reset to the default. 52 | 53 | ### The active range 54 | Styling will always be applied on the active range. When executing a function on a string, then that complete string will become the active range. If you use .append to add an other string, then that latest string will become the active range. When using the + sign then that will replaced by an append on 2 Attributer objects. All functions on those objects will first be performed before the append will be executed. So if you do an .all then still only one of the strings will be tha active range. You can use brackets to influence the order of execution. 55 | 56 | For instance here all text will be size 20 57 | 58 | ```swift 59 | ("red".red + "blue".blue).all.size(20) 60 | ``` 61 | 62 | And here only the text blue will be size 20 63 | 64 | ```swift 65 | "red".red + "blue".blue.all.size(20) 66 | ``` 67 | 68 | And like this all text will be size 20 69 | 70 | ```swift 71 | "red".red.append("blue").blue.all.size(20) 72 | ``` 73 | 74 | ### Clickable links 75 | When using AttributedTextView and creating links with .makeInteract, then you have to be aware that it will also automatically set the following properties which are needed for links to work. 76 | 77 | ```swift 78 | isUserInteractionEnabled = true 79 | isSelectable = true 80 | isEditable = false 81 | ``` 82 | 83 | ### Sample code 84 | 85 | Here is a sample of some basic functions: 86 | 87 | ```swift 88 | textView1.attributer = 89 | "1. ".red 90 | .append("This is the first test. ").green 91 | .append("Click on ").black 92 | .append("evict.nl").makeInteract { _ in 93 | UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in }) 94 | }.underline 95 | .append(" for testing links. ").black 96 | .append("Next test").underline.makeInteract { _ in 97 | print("NEXT") 98 | } 99 | .all.font(UIFont(name: "SourceSansPro-Regular", size: 16)) 100 | .setLinkColor(UIColor.purple) 101 | ``` 102 | 103 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample1.png?raw=true) 104 | 105 | Some more attributes and now using + instead of .append: 106 | 107 | ```swift 108 | textView1.attributer = 109 | "2. red, ".red.underline.underline(0x00ff00) 110 | + "green, ".green.fontName("Helvetica").size(30) 111 | + "cyan, ".cyan.size(22) 112 | + "orange, ".orange.kern(10) 113 | + "blue, ".blue.strikethrough(3).baselineOffset(8) 114 | + "black.".shadow(color: UIColor.gray, offset: CGSize(width: 2, height: 3), blurRadius: 3.0) 115 | ``` 116 | 117 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample2.png?raw=true) 118 | 119 | A match and matchAll sample: 120 | 121 | ```swift 122 | textView1.attributer = "It is this or it is that where the word is is selected".size(20) 123 | .match("is").underline.underline(UIColor.red) 124 | .matchAll("is").strikethrough(4) 125 | ``` 126 | 127 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample3.png?raw=true) 128 | 129 | A hashtags and mentions sample: 130 | 131 | ```swift 132 | textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library" 133 | .matchHashtags.underline 134 | .matchMentions 135 | .makeInteract { link in 136 | UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in }) 137 | } 138 | ``` 139 | 140 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample4.png?raw=true) 141 | 142 | A html sample: 143 | 144 | ```swift 145 | textView1.attributer = "My name is: Edwin
With a bulet list
".html 146 | ``` 147 | 148 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample6.png?raw=true) 149 | 150 | Some other text formating samples: 151 | 152 | ```swift 153 | textView1.attributer = ( 154 | "test stroke".strokeWidth(2).strokeColor(UIColor.red) 155 | + "test stroke 2\n".strokeWidth(2).strokeColor(UIColor.blue) 156 | + "test strikethrough".strikethrough(2).strikethroughColor(UIColor.red) 157 | + " test strikethrough 2\n".strikethrough(2).strikethroughColor(UIColor.yellow) 158 | + "letterpress ".letterpress 159 | + " obliquenes\n".obliqueness(0.4).backgroundColor(UIColor.cyan) 160 | + "expansion\n".expansion(0.8) 161 | ).all.size(24) 162 | ``` 163 | 164 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/Sample5.png?raw=true) 165 | 166 | 167 | Paragraph formatting: 168 | 169 | ```swift 170 | textView1.attributer = ( 171 | "The quick brown fox jumps over the lazy dog.\nPack my box with five dozen liquor jugs.\nSeveral fabulous dixieland jazz groups played with quick tempo." 172 | .paragraphLineHeightMultiple(5) 173 | .paragraphLineSpacing(6) 174 | .paragraphMinimumLineHeight(15) 175 | .paragraphMaximumLineHeight(50) 176 | .paragraphLineSpacing(10) 177 | .paragraphLineBreakModeWordWrapping 178 | .paragraphFirstLineHeadIndent(20) 179 | .paragraphApplyStyling 180 | ).all.size(12) 181 | ``` 182 | 183 | 184 | 185 | 186 | ### Use the attributedText functionality on a UILabel 187 | You can also use the Attributer for your UILabel. You only can't use the makeInteract function: 188 | 189 | ```swift 190 | let myUILabel = UILabel() 191 | myUILabel.attributedText = ("Just ".red + "some ".green + "text.".orange).attributedText 192 | ``` 193 | 194 | 195 | ### Extending AttributedTextView 196 | In the demo app you can see how you can extend the AttributedTextView with a custom property / function that will perform multiple actions. Here is a simple sample that will show you how you can create a myTitle property that will set multiple attributes: 197 | 198 | ```swift 199 | extension Attributer { 200 | open var myTitle: Attributer { 201 | get { 202 | return self.fontName("Arial").size(28).color(0xffaa66).kern(5) 203 | } 204 | } 205 | } 206 | 207 | public extension String { 208 | var myTitle: Attributer { 209 | get { 210 | return attributer.myTitle 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | ### Decorating the Attributed object 217 | In the demo app there is also a sample that shows you how you could decorate an Attributed object with default styling. 218 | 219 | ```swift 220 | attributedTextView.attributer = decorate(4) { content in return content 221 | + "This is our custom title".myTitle 222 | } 223 | ``` 224 | 225 | The decorate function can then look something like this: 226 | 227 | ```swift 228 | func decorate(_ id: Int, _ builder: ((_ content: Attributer) -> Attributer)) -> Attributer { 229 | var b = "Sample \(id + 1):\n\n".red 230 | b = builder(b) // Now add the content 231 | return b 232 | } 233 | ``` 234 | 235 | ### Creating your own custom label 236 | There is also an AttributedLabel class which derives from UILabel which makes it easy to create your own custom control that includes support for interfacebuilder. If you put a lable on a form in interfacebuilder and set it's class to your subclass (AttributedLabelSubclassDemo in the sample below) Then you will see the text formated in interface building according to what you have implemented in the configureAttributedLabel function. Here is a sample where a highlightText property is added so that a text can be constructed where that part is highlighted. 237 | 238 | ```swift 239 | import AttributedTextView 240 | import UIKit 241 | 242 | @IBDesignable open class AttributedLabelSubclassDemo: AttributedLabel { 243 | 244 | // Add this field in interfacebuilder and make sure the interface is updated after changes 245 | @IBInspectable var highlightText: String? { 246 | didSet { configureAttributedLabel() } 247 | } 248 | 249 | // Configure our custom styling. 250 | override open func configureAttributedLabel() { 251 | self.numberOfLines = 0 252 | if let highlightText = self.highlightText { 253 | self.attributedText = self.text?.green.match(highlightText).red.attributedText 254 | } else { 255 | self.attributedText = self.text?.green.attributedText 256 | } 257 | layoutIfNeeded() 258 | } 259 | } 260 | ``` 261 | 262 | In the demo app there is also a lable subclass for an icon list like this: 263 | ![animated](https://github.com/evermeer/AttributedTextView/blob/master/Screenshots/IconList.png?raw=true) 264 | 265 | You can find the code here: 266 | [Icon bulet list code](https://github.com/evermeer/AttributedTextView/blob/master/Demo/AttributedLabelSubclassDemo.swift#L52) 267 | 268 | 269 | ### Creating your own custom textview 270 | You could do the same as a label with the AttributedTextView (see previous paragraph). In the sample below 2 properties are entered into interfacebuilder the linkText is the part of the text what will be clickable and linkUrl will be the webpage that will be opened. 271 | 272 | ```swift 273 | import AttributedTextView 274 | import UIKit 275 | 276 | @IBDesignable class AttributedTextViewSubclassDemo: AttributedTextView { 277 | 278 | // Add this field in interfacebuilder and make sure the interface is updated after changes 279 | @IBInspectable var linkText: String? { 280 | didSet { configureAttributedTextView() } 281 | } 282 | 283 | // Add this field in interfacebuilder and make sure the interface is updated after changes 284 | @IBInspectable var linkUrl: String? { 285 | didSet { configureAttributedTextView() } 286 | } 287 | 288 | // Configure our custom styling. 289 | override func configureAttributedTextView() { 290 | if let text = self.text, let linkText = self.linkText, let linkUrl = self.linkUrl { 291 | self.attributer = text.green.match(linkText).makeInteract { _ in 292 | UIApplication.shared.open(URL(string: linkUrl)!, options: [:], completionHandler: { completed in }) 293 | } 294 | } else { 295 | self.attributer = (self.text ?? "").green 296 | } 297 | layoutIfNeeded() 298 | } 299 | } 300 | ``` 301 | 302 | 303 | ## Installation 304 | 305 | ### CocoaPods 306 | 307 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 308 | 309 | ```bash 310 | $ gem install cocoapods 311 | ``` 312 | 313 | > CocoaPods 1.1.0+ is required to build AttributedTextView 0.1.0+. 314 | 315 | To integrate AttributedTextView into your Xcode project using CocoaPods, specify it in your `Podfile`: 316 | 317 | ```ruby 318 | source 'https://github.com/CocoaPods/Specs.git' 319 | platform :ios, '8.0' 320 | use_frameworks! 321 | 322 | pod 'AttributedTextView', '~> 0.5.1' 323 | ``` 324 | 325 | Then, run the following command: 326 | 327 | ```bash 328 | $ pod install 329 | ``` 330 | 331 | ### Carthage 332 | 333 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application. 334 | 335 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 336 | 337 | ```bash 338 | $ brew update 339 | $ brew install carthage 340 | ``` 341 | 342 | To integrate AttributedTextView into your Xcode project using Carthage, specify it in your `Cartfile`: 343 | 344 | ```ogdl 345 | github "evermeer/AttributedTextView" ~> 0.5.1 346 | ``` 347 | ### Swift Package Manager 348 | 349 | To use AttributedTextView as a [Swift Package Manager](https://swift.org/package-manager/) package just add the following URL in your project file: 350 | 351 | ``` 352 | https://github.com/evermeer/AttributedTextView 353 | ``` 354 | 355 | And specify a version from `1.4.1` and onwards. 356 | 357 | ### Manually 358 | 359 | If you prefer not to use either of the aforementioned dependency managers, you can integrate AttributedTextView into your project manually. 360 | 361 | #### Git Submodules 362 | 363 | - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: 364 | 365 | ```bash 366 | $ git init 367 | ``` 368 | 369 | - Add AttributedTextView as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command: 370 | 371 | ```bash 372 | $ git submodule add https://github.com/evermeer/AttributedTextView.git 373 | $ git submodule update --init --recursive 374 | ``` 375 | 376 | - Open the new `AttributedTextView` folder, and drag the `AttributedTextView.xcodeproj` into the Project Navigator of your application's Xcode project. 377 | 378 | > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. 379 | 380 | - Select the `AttributedTextView.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. 381 | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. 382 | - In the tab bar at the top of that window, open the "General" panel. 383 | - Click on the `+` button under the "Embedded Binaries" section. 384 | - You will see two different `AttributedTextView.xcodeproj` folders each with two different versions of the `AttributedTextView.framework` nested inside a `Products` folder. 385 | 386 | > It does not matter which `Products` folder you choose from. 387 | 388 | - Select the `AttributedTextView.framework`. 389 | 390 | - And that's it! 391 | 392 | > The `AttributedTextView.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device. 393 | 394 | #### Embeded Binaries 395 | 396 | - Download the latest release from https://github.com/evermeer/AttributedTextView/releases 397 | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. 398 | - In the tab bar at the top of that window, open the "General" panel. 399 | - Click on the `+` button under the "Embedded Binaries" section. 400 | - Add the downloaded `AttributedTextView.framework`. 401 | - And that's it! 402 | 403 | ## License 404 | 405 | AttributedTextView is released under the MIT license. See [LICENSE](https://github.com/evermeer/AttributedTextView/blob/master/LICENSE) for details. 406 | 407 | ## My other libraries: 408 | Also see my other public source iOS libraries: 409 | 410 | - [EVReflection](https://github.com/evermeer/EVReflection) - Reflection based (Dictionary, CKRecord, JSON and XML) object mapping with extensions for Alamofire and Moya with RxSwift or ReactiveSwift 411 | - [EVCloudKitDao](https://github.com/evermeer/EVCloudKitDao) - Simplified access to Apple's CloudKit 412 | - [EVFaceTracker](https://github.com/evermeer/EVFaceTracker) - Calculate the distance and angle of your device with regards to your face in order to simulate a 3D effect 413 | - [EVURLCache](https://github.com/evermeer/EVURLCache) - a NSURLCache subclass for handling all web requests that use NSURLReques 414 | - [AlamofireOauth2](https://github.com/evermeer/AlamofireOauth2) - A swift implementation of OAuth2 using Alamofire 415 | - [EVWordPressAPI](https://github.com/evermeer/EVWordPressAPI) - Swift Implementation of the WordPress (Jetpack) API using AlamofireOauth2, AlomofireJsonToObjects and EVReflection (work in progress) 416 | - [PassportScanner](https://github.com/evermeer/PassportScanner) - Scan the MRZ code of a passport and extract the firstname, lastname, passport number, nationality, date of birth, expiration date and personal numer. 417 | - [AttributedTextView](https://github.com/evermeer/AttributedTextView) - Easiest way to create an attributed UITextView with support for multiple links (url, hashtags, mentions). 418 | 419 | 420 | -------------------------------------------------------------------------------- /Screenshots/IconList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/IconList.png -------------------------------------------------------------------------------- /Screenshots/Sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample1.png -------------------------------------------------------------------------------- /Screenshots/Sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample2.png -------------------------------------------------------------------------------- /Screenshots/Sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample3.png -------------------------------------------------------------------------------- /Screenshots/Sample4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample4.png -------------------------------------------------------------------------------- /Screenshots/Sample5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample5.png -------------------------------------------------------------------------------- /Screenshots/Sample6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evermeer/AttributedTextView/c937141724bb797ea29ea4b7055fa78f10911f36/Screenshots/Sample6.png -------------------------------------------------------------------------------- /Sources/AttributedLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedLabel.swift 3 | // AttributedTextView 4 | // 5 | // Created by Edwin Vermeer on 03/03/2017. 6 | // Copyright © 2017 evermeer. All rights reserved. 7 | // 8 | import UIKit 9 | 10 | /** 11 | Create your own lable class and use this class as it's base class. override the configureAttributedLabel function and set the self.attributedText to your prefered styling. For instance self.attributedText = self.text?.myHeader.attributedText See the samples for how you could add your own custom property for interface builder and alsu use that. 12 | */ 13 | @IBDesignable open class AttributedLabel: UILabel { 14 | override public init(frame: CGRect) { 15 | super.init(frame: frame) 16 | } 17 | 18 | required public init?(coder aDecoder: NSCoder) { 19 | super.init(coder: aDecoder) 20 | } 21 | 22 | open override func awakeFromNib() { 23 | super.awakeFromNib() 24 | configureAttributedLabel() 25 | } 26 | 27 | open override func prepareForInterfaceBuilder() { 28 | super.prepareForInterfaceBuilder() 29 | configureAttributedLabel() 30 | } 31 | 32 | override open var text: String? { 33 | didSet { 34 | configureAttributedLabel() 35 | } 36 | } 37 | 38 | open func configureAttributedLabel() { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/AttributedTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedTextView.h 3 | // AttributedTextView 4 | // 5 | // Created by Edwin Vermeer on 04/10/16. 6 | // Copyright © 2016 evermeer. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AttributedTextView. 12 | FOUNDATION_EXPORT double AttributedTextViewVersionNumber; 13 | 14 | //! Project version string for AttributedTextView. 15 | FOUNDATION_EXPORT const unsigned char AttributedTextViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /Sources/AttributedTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyLabel.swift 3 | // 4 | // Created by Edwin Vermeer on 25/11/2016. 5 | // Copyright © 2016 Edwin Vermeer. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | Set this class as the 'Custom Class' when you add a UITextView in the interfacebuilder. 12 | Use the attributer property for setting the attributed text. 13 | 14 | You can create your own textview class and use this class as it's base class. override the configureAttributedLabel function and set the self.attributer to your prefered styling. For instance self.attributer = self.text?.myHeader See the samples for how you could add your own custom property for interface builder and alsu use that. 15 | */ 16 | @IBDesignable open class AttributedTextView: UITextView, UITextViewDelegate { 17 | 18 | // required when using @IBDesignable 19 | override public init(frame: CGRect, textContainer: NSTextContainer?) { 20 | super.init(frame: frame, textContainer: textContainer) 21 | super.delegate = self 22 | } 23 | 24 | // required when using @IBDesignable 25 | required public init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | super.delegate = self 28 | } 29 | 30 | // Make sure configureAttributedTextView is called right after activation from the storyboard. 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | configureAttributedTextView() 34 | } 35 | 36 | // Make sure configureAttributedTextView is called inside interfacebuilder 37 | open override func prepareForInterfaceBuilder() { 38 | super.prepareForInterfaceBuilder() 39 | configureAttributedTextView() 40 | } 41 | 42 | // just an override for triggering configureAttributedTextView 43 | override open var text: String? { 44 | didSet { 45 | configureAttributedTextView() 46 | } 47 | } 48 | 49 | // For enabeling the size adjustment 50 | @IBInspectable open var forceIntrinsicContentSizeToBeContentSize: Bool = false { 51 | didSet { configureAttributedTextView() } 52 | } 53 | 54 | // Return the contentSize if its forced enabled 55 | override open var intrinsicContentSize: CGSize { 56 | return self.forceIntrinsicContentSizeToBeContentSize ? self.contentSize : super.intrinsicContentSize 57 | } 58 | 59 | // Subclass AttributedTextView and override this function if you want to use easy custum controls in interface builder 60 | open func configureAttributedTextView() { 61 | } 62 | 63 | // storage variable for the Attributer 64 | private var _attributer: Attributer? 65 | 66 | /** 67 | The attributer object that will set the attributedText 68 | */ 69 | open var attributer: Attributer { 70 | get { 71 | if _attributer == nil { 72 | _attributer = Attributer("") 73 | } 74 | return _attributer! 75 | } 76 | set { 77 | _attributer = newValue 78 | self.attributedText = _attributer?.attributedText 79 | 80 | if _attributer?.hasCallbacks() ?? false { 81 | // Without these makeInteract does not work 82 | self.isUserInteractionEnabled = true 83 | self.isSelectable = true 84 | self.isEditable = false 85 | } 86 | if let color = _attributer?.linkColor { 87 | self.linkTextAttributes = [NSAttributedString.Key.foregroundColor: color as Any] 88 | } 89 | } 90 | } 91 | 92 | /** 93 | If you manually set the delegate on the AttributedTextView, then it will set this property instead of the actual delegate. The actual delegate will be set to this class itself for handling the interactions on the links. events will be forwarded to the _delegate. 94 | */ 95 | public var _delegate: UITextViewDelegate? 96 | /** 97 | Delegate that can be set for forwarding events from the UITextView 98 | */ 99 | override open var delegate: UITextViewDelegate? { 100 | get { 101 | return super.delegate 102 | } 103 | set { 104 | _delegate = newValue 105 | } 106 | } 107 | 108 | 109 | // MARK: - UITextViewDelegate functions - forwarding all to _delegate 110 | 111 | /** 112 | UITextViewDelegate function for forwarding the textViewShoudlBeginEditing 113 | 114 | -property textView: The UITextView where the delegate is called on 115 | */ 116 | public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { 117 | return _delegate?.textViewShouldBeginEditing?(textView) ?? self.isEditable 118 | } 119 | 120 | /** 121 | UITextViewDelegate function for forwarding the textViewShouldEndEditing 122 | 123 | -property textView: The UITextView where the delegate is called on 124 | */ 125 | public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { 126 | return _delegate?.textViewShouldEndEditing?(textView) ?? self.isEditable 127 | } 128 | 129 | /** 130 | UITextViewDelegate function for forwarding the textViewDidBeginEditing 131 | 132 | -property textView: The UITextView where the delegate is called on 133 | */ 134 | public func textViewDidBeginEditing(_ textView: UITextView) { 135 | _delegate?.textViewDidBeginEditing?(textView) 136 | } 137 | 138 | /** 139 | UITextViewDelegate function for forwarding the textViewDidEndEditing 140 | 141 | -property textView: The UITextView where the delegate is called on 142 | */ 143 | public func textViewDidEndEditing(_ textView: UITextView) { 144 | _delegate?.textViewDidEndEditing?(textView) 145 | } 146 | 147 | 148 | /** 149 | UITextViewDelegate function for forwarding the shouldChangeTextIn range 150 | 151 | -property textView: The UITextView where the delegate is called on 152 | -property shouldChangeTextIn: the range 153 | -property replacementText: the replacement text 154 | */ 155 | public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 156 | return _delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) ?? self.isEditable 157 | } 158 | 159 | /** 160 | UITextViewDelegate function for forwarding the textViewDidChange 161 | 162 | -property textView: The UITextView where the delegate is called on 163 | */ 164 | public func textViewDidChange(_ textView: UITextView) { 165 | _delegate?.textViewDidChange?(textView) 166 | } 167 | 168 | /** 169 | UITextViewDelegate function for forwarding the textViewDidChangeSelection 170 | 171 | -property textView: The UITextView where the delegate is called on 172 | */ 173 | public func textViewDidChangeSelection(_ textView: UITextView) { 174 | _delegate?.textViewDidChangeSelection?(textView) 175 | } 176 | 177 | /** 178 | UITextViewDelegate function for forwarding the shouldInteractWith URL 179 | 180 | -property textView: The UITextView where the delegate is called on 181 | -property shouldInteractWith: The URL to interact with 182 | -property characterRagne: the NSRange for the selection 183 | -property interaction: The UITextItemInteraction 184 | */ 185 | @available(iOS 10.0, *) 186 | public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { 187 | _attributer?.interactWithURL(URL: URL) 188 | return _delegate?.textView?(textView, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? false 189 | } 190 | 191 | /** 192 | UITextViewDelegate function for forwarding the shouldInteractWith textAttachment 193 | 194 | -property textView: The UITextView where the delegate is called on 195 | -property shouldInteractWith: the NSTextAttachement 196 | -property characterRange: the NSRange for the selection 197 | -property interaction: The UITextItemInteraction 198 | */ 199 | @available(iOS 10.0, *) 200 | public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { 201 | return _delegate?.textView?(textView, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) ?? false 202 | } 203 | 204 | /** 205 | UITextViewDelegate function for forwarding the shouldInteractWith URL 206 | 207 | -property textView: The UITextView where the delegate is called on 208 | -property shouldInteractWith: the NSTextAttachement 209 | -property characterRange: the NSRange for the selection 210 | */ 211 | @available(iOS, introduced: 7.0, deprecated: 10.0, message: "Use textView:shouldInteractWithURL:inRange:forInteractionType: instead") 212 | public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { 213 | _attributer?.interactWithURL(URL: URL) 214 | return _delegate?.textView!(textView, shouldInteractWith: URL, in: characterRange) ?? false 215 | } 216 | 217 | /** 218 | UITextViewDelegate function for forwarding the shouldInteractWith textAttachment 219 | 220 | -property textView: The UITextView where the delegate is called on 221 | -property shouldInteractWith: the NSTextAttachement 222 | -property characterRange: the NSRange for the selection 223 | */ 224 | @available(iOS, introduced: 7.0, deprecated: 10.0, message: "Use textView:shouldInteractWithTextAttachment:inRange:forInteractionType: instead") 225 | public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool { 226 | return _delegate?.textView?(textView, shouldInteractWith: textAttachment, in: characterRange) ?? false 227 | } 228 | } 229 | 230 | 231 | -------------------------------------------------------------------------------- /Sources/Attributer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attributer.swift 3 | // 4 | // Created by Edwin Vermeer on 25/11/2016. 5 | // Copyright © 2016 Edwin Vermeer. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | The core of working with attributed strings 12 | */ 13 | open class Attributer { 14 | 15 | 16 | /** 17 | You can get the final NSMutableAttributedString from here 18 | */ 19 | 20 | open var attributedText: NSMutableAttributedString 21 | /** 22 | Used to set the link color on the UITextView 23 | */ 24 | public var linkColor: UIColor? 25 | 26 | /** 27 | Save all the callbacks (from the .makeInteract) 28 | */ 29 | fileprivate var urlCallbacks: [String : ((_ link: String) -> ())] = [:] 30 | 31 | /** 32 | Do we have interactions 33 | */ 34 | public func hasCallbacks() -> Bool { 35 | return urlCallbacks.count > 0 36 | } 37 | 38 | /** 39 | The current active ranges that will be influenced by all functions. 40 | */ 41 | fileprivate var ranges: [NSRange] = [] { 42 | didSet { 43 | paragraphStyle = NSMutableParagraphStyle() 44 | } 45 | } 46 | 47 | /** 48 | The current active paragraphStyle 49 | */ 50 | fileprivate var paragraphStyle = NSMutableParagraphStyle() 51 | 52 | 53 | /** 54 | Contructor method for Attributer 55 | 56 | Instantiate a new Attributer based on an attributed string 57 | 58 | -parameter string: The NSMutableAttributedString that will be used as the initial value of Attributer. 59 | */ 60 | public init(_ string: NSMutableAttributedString) { 61 | self.attributedText = string 62 | ranges.append(NSRange(location: 0, length: self.attributedText.length)) 63 | } 64 | 65 | /** 66 | Contructor method for Attributer 67 | 68 | Instantiate a new Attributer based on an attributed string (converted to a mutable first) 69 | 70 | -parameter string: The NSAttributedString that will be used as the initial value of Attributer. 71 | */ 72 | public convenience init(_ string: NSAttributedString) { 73 | self.init(NSMutableAttributedString(attributedString: string)) 74 | } 75 | 76 | /** 77 | Contructor method for Attributer 78 | 79 | Instantiate a new Attributer based on a string 80 | 81 | -parameter string: The NSString that will be used as the initial value of Attributer. 82 | */ 83 | public convenience init(_ string: NSString) { 84 | self.init(NSMutableAttributedString(string: string as String)) 85 | } 86 | 87 | /** 88 | Contructor method for Attributer 89 | 90 | Instantiate a new Attributer based on a string 91 | 92 | -parameter string: The String that will be used as the initial value of Attributer. 93 | */ 94 | public convenience init(_ string: String) { 95 | self.init(NSMutableAttributedString(string: string)) 96 | } 97 | 98 | /** 99 | Contructor method for Attributer 100 | 101 | Instantiate a new Attributer based on a string 102 | 103 | -parameter string: The String that will be used as the initial value of Attributer. 104 | */ 105 | public convenience init(_ image: UIImage, bounds: CGRect? = nil) { 106 | let imageAttachment = NSTextAttachment() 107 | imageAttachment.image = image 108 | if let bounds = bounds { 109 | imageAttachment.bounds = bounds 110 | } 111 | self.init(NSAttributedString(attachment: imageAttachment)) 112 | } 113 | 114 | 115 | //MARK - Color functions 116 | 117 | 118 | /** 119 | Apply the color black to the active range(es) 120 | */ 121 | open var black: Attributer { 122 | get { 123 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.black) 124 | } 125 | } 126 | 127 | /** 128 | Apply the color darkGray to the active range(es) 129 | */ 130 | open var darkGray: Attributer { 131 | get { 132 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.darkGray) 133 | } 134 | } 135 | 136 | /** 137 | Apply the color lightGray to the active range(es) 138 | */ 139 | open var lightGray: Attributer { 140 | get { 141 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.lightGray) 142 | } 143 | } 144 | 145 | /** 146 | Apply the color white to the active range(es) 147 | */ 148 | open var white: Attributer { 149 | get { 150 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.white) 151 | } 152 | } 153 | 154 | /** 155 | Apply the color gray to the active range(es) 156 | */ 157 | open var gray: Attributer { 158 | get { 159 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.gray) 160 | } 161 | } 162 | 163 | /** 164 | Apply the color red to the active range(es) 165 | */ 166 | open var red: Attributer { 167 | get { 168 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.red) 169 | } 170 | } 171 | 172 | /** 173 | Apply the color green to the active range(es) 174 | */ 175 | open var green: Attributer { 176 | get { 177 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.green) 178 | } 179 | } 180 | 181 | /** 182 | Apply the color blue to the active range(es) 183 | */ 184 | open var blue: Attributer { 185 | get { 186 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.blue) 187 | } 188 | } 189 | 190 | /** 191 | Apply the color cyan to the active range(es) 192 | */ 193 | open var cyan: Attributer { 194 | get { 195 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.cyan) 196 | } 197 | } 198 | 199 | /** 200 | Apply the color yellow to the active range(es) 201 | */ 202 | open var yellow: Attributer { 203 | get { 204 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.yellow) 205 | } 206 | } 207 | 208 | /** 209 | Apply the color magenta to the active range(es) 210 | */ 211 | open var magenta: Attributer { 212 | get { 213 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.magenta) 214 | } 215 | } 216 | 217 | /** 218 | Apply the color orange to the active range(es) 219 | */ 220 | open var orange: Attributer { 221 | get { 222 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.orange) 223 | } 224 | } 225 | 226 | /** 227 | Apply the color purple to the active range(es) 228 | */ 229 | open var purple: Attributer { 230 | get { 231 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.purple) 232 | } 233 | } 234 | 235 | /** 236 | Apply the color brown to the active range(es) 237 | */ 238 | open var brown: Attributer { 239 | get { 240 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.brown) 241 | } 242 | } 243 | 244 | /** 245 | Apply the color clear to the active range(es) 246 | */ 247 | open var clear: Attributer { 248 | get { 249 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: UIColor.clear) 250 | } 251 | } 252 | 253 | /** 254 | Apply a UIColor to the active range(es) 255 | 256 | -parameter color: The UIColor that will be applied. 257 | */ 258 | open func color(_ color: UIColor) -> Attributer { 259 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: color) 260 | } 261 | 262 | /** 263 | Apply a color from a hex value to the active range(es) 264 | 265 | -parameter hex: The hex value of the color that will be applied. 266 | */ 267 | open func color(_ hex: Int) -> Attributer { 268 | return applyAttributes(NSAttributedString.Key.foregroundColor.rawValue, value: colorFrom(hex: hex)) 269 | } 270 | 271 | // MARK - Selection functions 272 | 273 | 274 | /** 275 | Make the active range the entire text 276 | */ 277 | open var all: Attributer { 278 | get { 279 | ranges.removeAll() 280 | ranges.append(NSRange(location: 0, length: self.attributedText.length)) 281 | return self 282 | } 283 | } 284 | 285 | /** 286 | Make the active range (from and to) 287 | 288 | -parameter from: The start of the new range 289 | -parameter to: The end of the new range. 290 | */ 291 | open func range(_ from: Int, to: Int) -> Attributer { 292 | ranges.removeAll() 293 | ranges.append(NSRange(location: from, length: to - from)) 294 | return self 295 | } 296 | 297 | /** 298 | Make the active range (location and length) 299 | 300 | -parameter location: The location of the new range 301 | -parameter length: The length of the new range. 302 | */ 303 | open func range(_ location: Int, length: Int) -> Attributer { 304 | ranges.removeAll() 305 | ranges.append(NSRange(location: location, length: length)) 306 | return self 307 | } 308 | 309 | 310 | /** 311 | Just set the active range 312 | 313 | -parameter range: The new range. 314 | */ 315 | open func range(_ range: NSRange) -> Attributer { 316 | ranges.removeAll() 317 | ranges.append(range) 318 | return self 319 | } 320 | 321 | /** 322 | Find the first occurrance of a string (search with using .CompareOptions) 323 | 324 | -parameter substring: The string to search for. 325 | -parameter options: The search options 326 | */ 327 | open func matchWithOptions(_ substring: String, _ options: NSString.CompareOptions = .literal) -> Attributer { 328 | let string = self.attributedText.string as NSString 329 | ranges.removeAll() 330 | ranges.append(string.range(of: substring, options: options)) 331 | return self 332 | } 333 | 334 | /** 335 | Find the first occurrance of a string 336 | 337 | -parameter substring: The string to search for. 338 | */ 339 | open func match(_ substring: String) -> Attributer { 340 | return matchWithOptions(substring) 341 | } 342 | 343 | /** 344 | Find the all occurrances of a string (search with using .CompareOptions) 345 | 346 | -parameter substring: The string to search for. 347 | -parameter options: The search options 348 | */ 349 | open func matchAllWithOptions(_ substring: String, _ options: NSString.CompareOptions = .literal) -> Attributer { 350 | let string = self.attributedText.string as NSString 351 | var range = string.range(of: substring, options: options) 352 | ranges.removeAll() 353 | ranges.append(range) 354 | while range.length != 0 { 355 | let location = range.location + range.length 356 | let length = string.length - location 357 | range = string.range(of: substring, options: options, range: NSRange(location: location, length: length)) 358 | ranges.append(range) 359 | } 360 | return self 361 | } 362 | 363 | /** 364 | Find the all occurrances of a string 365 | 366 | -parameter substring: The string to search for. 367 | */ 368 | open func matchAll(_ substring: String) -> Attributer { 369 | return matchAllWithOptions(substring) 370 | } 371 | 372 | /** 373 | Find the all occurrances of any of the strings (search with using .CompareOptions) 374 | 375 | -parameter substring: An array of string to search for. 376 | -parameter options: The search options 377 | */ 378 | open func matchAnyWithOptions(_ substrings: [String], _ options: NSString.CompareOptions = .literal) -> Attributer { 379 | let string = self.attributedText.string as NSString 380 | ranges.removeAll() 381 | for substring in substrings { 382 | var range = string.range(of: substring, options: options) 383 | ranges.append(range) 384 | while range.length != 0 { 385 | let location = range.location + range.length 386 | let length = string.length - location 387 | range = string.range(of: substring, options: options, range: NSRange(location: location, length: length)) 388 | ranges.append(range) 389 | } 390 | } 391 | return self 392 | } 393 | 394 | /** 395 | Find the all occurrances of any of the strings 396 | 397 | -parameter substring: The array of strings to search for. 398 | */ 399 | open func matchAny(_ substrings: [String]) -> Attributer { 400 | return matchAnyWithOptions(substrings) 401 | } 402 | 403 | /** 404 | Find the all hashtags (words beginning with #) 405 | */ 406 | open var matchHashtags: Attributer { 407 | get { 408 | return matchPattern("#[\\p{L}0-9_]*") 409 | } 410 | } 411 | 412 | /** 413 | Find the all mentions (words beginning with @) 414 | */ 415 | open var matchMentions: Attributer { 416 | get { 417 | return matchPattern("@[\\p{L}0-9_]*") 418 | } 419 | } 420 | 421 | /** 422 | Find the all links 423 | */ 424 | open var matchLinks: Attributer { 425 | get { 426 | return matchPattern("(([\\w]+:)?//)?(([\\d\\w]|%[a-fA-f\\d]{2,2})+(:([\\d\\w]|%[a-fA-f\\d]{2,2})+)?@)?([\\d\\w][-\\d\\w]{0,253}[\\d\\w]\\.)+[\\w]{2,63}(:[\\d]+)?(/([-+_~.\\d\\w]|%[a-fA-f\\d]{2,2})*)*(\\?(&?([-+_~.\\d\\w]|%[a-fA-f\\d]{2,2})=?)*)?(#([-+_~.\\d\\w]|%[a-fA-f\\d]{2,2})*)?") 427 | } 428 | } 429 | 430 | /** 431 | Use a regular expression patter to find ranges 432 | 433 | -parameter pattern: The regex pattern. 434 | */ 435 | open func matchPattern(_ pattern: String) -> Attributer { 436 | guard let elementRegex: NSRegularExpression = Attributer.regularExpression(for: pattern) else { return self } 437 | let range = NSRange(location: 0, length: (self.attributedText.string as NSString).length) // .characters.count is 438 | let results: [NSTextCheckingResult] = elementRegex.matches(in: self.attributedText.string, options: [], range: range) 439 | ranges = results.map { $0.range } 440 | return self 441 | } 442 | 443 | /** 444 | Append a string to the current attributed text and select the new string as the active range 445 | 446 | -parameter string: The string that will be added 447 | */ 448 | open func append(_ string: String) -> Attributer { 449 | return self.append(NSMutableAttributedString(string: string)) 450 | } 451 | 452 | /** 453 | Append a string to the current attributed text and select the new string as the active range 454 | 455 | -parameter string: The string that will be added 456 | */ 457 | open func appendHtml(_ string: String) -> Attributer { 458 | return self.append(NSMutableAttributedString(html: string) ?? NSMutableAttributedString()) 459 | } 460 | 461 | /** 462 | Append an attributed string to the current attributed text and select the new string as the active range 463 | 464 | -parameter attributedString: The attributed string that will be added 465 | */ 466 | open func append(_ attributedString: NSMutableAttributedString) -> Attributer { 467 | ranges.removeAll() 468 | let rememberLength = self.attributedText.length 469 | self.attributedText.append(attributedString) 470 | ranges.append(NSRange(location: rememberLength, length: attributedString.length)) 471 | return self 472 | } 473 | 474 | /** 475 | Append an Attributer to the current and select the new string as the active range 476 | 477 | -parameter attributer: The Attributer that will be added 478 | */ 479 | open func append(_ attributer: Attributer) -> Attributer { 480 | for callback in attributer.urlCallbacks { 481 | self.urlCallbacks[callback.key] = callback.value 482 | } 483 | self.linkColor = attributer.linkColor ?? self.linkColor 484 | return self.append(attributer.attributedText) 485 | 486 | } 487 | 488 | 489 | 490 | // MARK: - Font 491 | 492 | /** 493 | Apply a font to the active range 494 | 495 | -parameter fontName: The name of the font that will be applied 496 | */ 497 | open func fontName(_ fontName: String) -> Attributer { 498 | for range in self.ranges { 499 | guard range.location != NSNotFound else { return self } 500 | let substring = self.attributedText.attributedSubstring(from: range) 501 | if substring.length > 0, let font = substring.attribute(NSAttributedString.Key.font, at: 0, effectiveRange:nil) as? UIFont { 502 | if let currentFont = UIFont(name: fontName, size: font.pointSize) { 503 | self.attributedText.addAttribute(NSAttributedString.Key.font, value: currentFont, range: range) 504 | } 505 | } else { 506 | if let currentFont = UIFont(name: fontName, size: UIFont.systemFontSize) { 507 | self.attributedText.addAttribute(NSAttributedString.Key.font, value: currentFont, range: range) 508 | } 509 | } 510 | } 511 | return self 512 | } 513 | 514 | /** 515 | Apply a fontsize to the active range 516 | 517 | -parameter size: The fontsize that will be applied 518 | */ 519 | open func size(_ size: CGFloat) -> Attributer { 520 | for range in self.ranges { 521 | guard range.location != NSNotFound else { return self } 522 | let substring = self.attributedText.attributedSubstring(from: range) 523 | if substring.length > 0, let font = substring.attribute(NSAttributedString.Key.font, at: 0, effectiveRange:nil) as? UIFont { 524 | self.attributedText.addAttribute(NSAttributedString.Key.font, value: UIFont(name: font.fontName, size: size)!, range: range) 525 | } else { 526 | self.attributedText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: size), range: range) 527 | } 528 | } 529 | return self 530 | } 531 | 532 | /** 533 | Apply a font to the active range 534 | 535 | -parameter font: The UIFont that will be applied 536 | */ 537 | open func font(_ font: UIFont?) -> Attributer { 538 | if font != nil { 539 | return fontName(font!.fontName).size(font!.pointSize) 540 | } 541 | return self 542 | } 543 | 544 | 545 | // MARK: - Create a link 546 | 547 | /** 548 | Make the current active range interact. A touch will call the UITextViewDelegate which is handled in the AttributedTextView 549 | 550 | -parameter scheme: The scheme that will be added in front of the link 551 | */ 552 | @available(*, deprecated, message: "use AttributedTextView with makeInteract: instead") 553 | open func makeInteractWithURLforScheme(_ scheme: String) -> Attributer { 554 | for nsRange in self.ranges { 555 | guard nsRange.location != NSNotFound else { return self } 556 | let iRange = self.attributedText.string.range(from: nsRange) 557 | if let escapedString = self.attributedText.string.substring(with: iRange!).addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlHostAllowed) { 558 | self.attributedText.addAttribute(NSAttributedString.Key.link, value: "\(scheme):\(escapedString)", range: nsRange) 559 | } 560 | } 561 | return self 562 | } 563 | 564 | /** 565 | Make the current active range interact. A touch will call the UITextViewDelegate. When using the AttributedTextView the callback function will be called 566 | 567 | -parameter callback: The callback function that will be called when using AttributedTextView 568 | */ 569 | open func makeInteract(_ callback: @escaping ((_ link: String) -> ())) -> Attributer { 570 | for nsRange in self.ranges { 571 | guard nsRange.location != NSNotFound else { return self } 572 | let unEscapedString = (self.attributedText.string as NSString).substring(with: nsRange) 573 | let escapedString = unEscapedString.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlHostAllowed) ?? "" 574 | self.attributedText.addAttribute(NSAttributedString.Key.link, value: "AttributedTextView:\(escapedString)", range: nsRange) 575 | urlCallbacks[unEscapedString] = callback 576 | 577 | } 578 | return self 579 | } 580 | 581 | /** 582 | Used for setting the link color of the UITextView 583 | 584 | -parameter color: The color of all links 585 | */ 586 | open func setLinkColor(_ color: UIColor) -> Attributer { 587 | linkColor = color 588 | return self 589 | } 590 | 591 | /** 592 | Called by AttributedTextView when a URL is touched 593 | 594 | -parameter URL: The URL that was touched 595 | */ 596 | public func interactWithURL(URL: URL) { 597 | let unescapedString = URL.absoluteString.replacingOccurrences(of: "AttributedTextView:", with: "").removingPercentEncoding ?? "" 598 | urlCallbacks[unescapedString]?(unescapedString) 599 | } 600 | 601 | /** 602 | Make the current selected range a link 603 | 604 | -parameter link: The URL that will be forwarded to the UITextViewDelegate 605 | */ 606 | open func link(_ link: URL) -> Attributer { 607 | return applyAttributes(NSAttributedString.Key.link.rawValue, value: link as AnyObject) 608 | } 609 | 610 | /** 611 | Make the current selected range a link 612 | 613 | -parameter link: The NSString that will be forwarded to the UITextViewDelegate 614 | */ 615 | open func link(_ link: NSString) -> Attributer { 616 | return applyAttributes(NSAttributedString.Key.link.rawValue, value: link) 617 | } 618 | 619 | // MARK: - Style 620 | 621 | /** 622 | underline the active range 623 | */ 624 | open var underline: Attributer { 625 | return underline(NSUnderlineStyle.single) 626 | } 627 | 628 | /** 629 | set the underline style for the active range (you also have to call .underline) 630 | 631 | -parameter underline: The underline style 632 | */ 633 | open func underline(_ style: NSUnderlineStyle) -> Attributer { 634 | return applyAttributes(NSAttributedString.Key.underlineStyle.rawValue, value: style.rawValue as AnyObject) 635 | } 636 | 637 | /** 638 | set the underline style for the active range (you also have to call .underline) 639 | 640 | -parameter style: The underline style 641 | -parameter pattern : The underline pattern 642 | */ 643 | open func underline(_ style: NSUnderlineStyle, _ pattern: NSUnderlineStyle) -> Attributer { 644 | return applyAttributes(NSAttributedString.Key.underlineStyle.rawValue, value: NSNumber(value: (style.rawValue | pattern.rawValue))) 645 | } 646 | 647 | /** 648 | set the underline color for the active range (you also have to call .underline) 649 | 650 | -parameter color: the UIColor of the undeline 651 | */ 652 | open func underline(_ color: UIColor) -> Attributer { 653 | return applyAttributes(NSAttributedString.Key.underlineColor.rawValue, value: color) 654 | } 655 | 656 | /** 657 | set the underline color for the active range (you also have to call .underline) 658 | 659 | -parameter hex: the hex value of the color for the underline 660 | */ 661 | open func underline(_ hex: Int) -> Attributer { 662 | return applyAttributes(NSAttributedString.Key.underlineColor.rawValue, value: colorFrom(hex: hex)) 663 | } 664 | 665 | /** 666 | Attach an image to the range 667 | 668 | -parameter image: the UIImage that will be used as the attachment 669 | */ 670 | open func attach(_ image: UIImage?, bounds: CGRect? = nil) -> Attributer { 671 | let imageAttachment = NSTextAttachment() 672 | imageAttachment.image = image 673 | if let bounds = bounds { 674 | imageAttachment.bounds = bounds 675 | } 676 | self.attributedText.append(NSAttributedString(attachment: imageAttachment)) 677 | return self 678 | } 679 | 680 | /** 681 | Attach an image to the range 682 | 683 | -parameter imageStr: the name of the image that will be used as the attachment 684 | */ 685 | open func attach(_ imageStr: String, bounds: CGRect? = nil) -> Attributer { 686 | let imageAttachment = NSTextAttachment() 687 | imageAttachment.image = UIImage(named: imageStr) 688 | if let bounds = bounds { 689 | imageAttachment.bounds = bounds 690 | } 691 | self.attributedText.append(NSAttributedString(attachment: imageAttachment)) 692 | return self 693 | } 694 | 695 | /** 696 | Set the shadow for the active range(es) 697 | 698 | -parameter shadow: The NSShadow that will be set 699 | */ 700 | open func shadow(_ shadow: NSShadow) -> Attributer { 701 | return applyAttributes(NSAttributedString.Key.shadow.rawValue, value: shadow) 702 | } 703 | 704 | /** 705 | Set the shadow for the active range(es) 706 | 707 | -parameter color: The UIColor for the shadow 708 | -parameter offset: The CGSize offset for the shadow 709 | -parameter blurRadius: The blurRadius for the shadow 710 | */ 711 | open func shadow(color: UIColor?, offset: CGSize, blurRadius: CGFloat) -> Attributer { 712 | let shadow = NSShadow() 713 | shadow.shadowColor = color 714 | shadow.shadowOffset = offset 715 | shadow.shadowBlurRadius = blurRadius 716 | return applyAttributes(NSAttributedString.Key.shadow.rawValue, value: shadow) 717 | } 718 | 719 | /** 720 | Set the baseline offset for the active range(es) 721 | This will move the text up or down 722 | 723 | -parameter offset: The number of pixels that the text will be moved up or down. 724 | */ 725 | open func baselineOffset(_ offset: NSNumber) -> Attributer { 726 | return applyAttributes(NSAttributedString.Key.baselineOffset.rawValue, value: offset) 727 | } 728 | 729 | /** 730 | Set the kern for the active range(es) 731 | This will set the spacing between letters 732 | 733 | -parameter number: The number of pixels that will be between the letters. 734 | */ 735 | open func kern(_ number: NSNumber) -> Attributer { 736 | return applyAttributes(NSAttributedString.Key.kern.rawValue, value: number) 737 | } 738 | 739 | /** 740 | Set the striketrhough for the active range(es) 741 | 742 | -parameter number: The number of pixels the strikethrough will be high. 743 | */ 744 | open func strikethrough(_ number: NSNumber) -> Attributer { 745 | return applyAttributes(NSAttributedString.Key.strikethroughStyle.rawValue, value: number) 746 | } 747 | 748 | /** 749 | Set the striketrhough color 750 | 751 | -parameter color: The strikethrough color. 752 | */ 753 | open func strikethroughColor(_ color: UIColor) -> Attributer { 754 | return applyAttributes(NSAttributedString.Key.strikethroughColor.rawValue, value: color) 755 | } 756 | 757 | /** 758 | Set the stroke color 759 | 760 | -parameter color: The stroke color. 761 | */ 762 | open func strokeColor(_ color: UIColor) -> Attributer { 763 | return applyAttributes(NSAttributedString.Key.strokeColor.rawValue, value: color) 764 | } 765 | 766 | /** 767 | Set the stroke width 768 | 769 | -parameter number: The stroke width. 770 | */ 771 | open func strokeWidth(_ number: NSNumber) -> Attributer { 772 | return applyAttributes(NSAttributedString.Key.strokeWidth.rawValue, value: number) 773 | } 774 | 775 | /** 776 | Set the style to letterpress 777 | */ 778 | open var letterpress: Attributer { 779 | get { 780 | return applyAttributes(NSAttributedString.Key.textEffect.rawValue, value: NSAttributedString.TextEffectStyle.letterpressStyle as NSString) 781 | } 782 | } 783 | 784 | /** 785 | Set the obliqueness 786 | 787 | -parameter number: The obliqueness. 788 | */ 789 | open func obliqueness(_ number: NSNumber) -> Attributer { 790 | return applyAttributes(NSAttributedString.Key.obliqueness.rawValue, value: number) 791 | } 792 | 793 | /** 794 | Set the expansion 795 | 796 | -parameter number: The expansion. 797 | */ 798 | open func expansion(_ number: NSNumber) -> Attributer { 799 | return applyAttributes(NSAttributedString.Key.expansion.rawValue, value: number) 800 | } 801 | 802 | /** 803 | "In iOS, horizontal text is always used and specifying a different value is undefined." 804 | 805 | Set the vertical Glyph form 806 | 807 | -parameter number: The vertical glyph form. 808 | open func verticalGlyphForm(_ number: NSNumber) -> Attributer { 809 | return applyAttributes(NSVerticalGlyphFormAttributeName, value: number) 810 | } 811 | */ 812 | 813 | /** 814 | Set the backgroundColor 815 | 816 | -parameter color: The color. 817 | */ 818 | open func backgroundColor(_ color: UIColor) -> Attributer { 819 | return applyAttributes(NSAttributedString.Key.backgroundColor.rawValue, value: color) 820 | } 821 | 822 | /** 823 | Set the ligature 824 | 825 | -parameter number: The ligature. 826 | */ 827 | open func ligature(_ number: NSNumber) -> Attributer { 828 | return applyAttributes(NSAttributedString.Key.ligature.rawValue, value: number) 829 | } 830 | 831 | /** 832 | Set the attachment 833 | 834 | -parameter attachment: The attachment. 835 | */ 836 | open func attachment(_ attachment: NSTextAttachment) -> Attributer { 837 | return applyAttributes(NSAttributedString.Key.attachment.rawValue, value: attachment) 838 | } 839 | 840 | /** 841 | Set the writing directions 842 | 843 | -parameter directions: The directions. 844 | */ 845 | open func writingDirection(_ directions: [NSNumber]) -> Attributer { 846 | return applyAttributes(NSAttributedString.Key.writingDirection.rawValue, value: directions as AnyObject) 847 | } 848 | 849 | 850 | // MARK: - Paragraph functions 851 | 852 | /** 853 | Set the paragraph 854 | 855 | -parameter paragraph: The paragraph style. 856 | */ 857 | open func paragraph(_ paragraph: NSMutableParagraphStyle) -> Attributer { 858 | return applyAttributes(NSAttributedString.Key.paragraphStyle.rawValue, value: paragraph) 859 | } 860 | 861 | /** 862 | Apply the paragraph stylings that have been set by all the paragraph functions 863 | */ 864 | open var paragraphApplyStyling: Attributer { 865 | get { 866 | return paragraph(paragraphStyle) 867 | } 868 | } 869 | 870 | /** 871 | Align the paragraph in the center 872 | */ 873 | open var paragraphAlignCenter: Attributer { 874 | get { 875 | paragraphStyle.alignment = NSTextAlignment.center 876 | return self 877 | } 878 | } 879 | 880 | /** 881 | Align the paragraph right 882 | */ 883 | open var paragraphAlignRight: Attributer { 884 | get { 885 | paragraphStyle.alignment = NSTextAlignment.right 886 | return self 887 | } 888 | } 889 | 890 | /** 891 | Align the paragraph left 892 | */ 893 | open var paragraphAlignLeft: Attributer { 894 | get { 895 | paragraphStyle.alignment = NSTextAlignment.left 896 | return self 897 | } 898 | } 899 | 900 | /** 901 | Align the paragraph justified 902 | */ 903 | open var paragraphAlignJustified: Attributer { 904 | get { 905 | paragraphStyle.alignment = NSTextAlignment.justified 906 | return self 907 | } 908 | } 909 | 910 | /** 911 | Align the paragraph Natural 912 | */ 913 | open var paragraphAlignNatural: Attributer { 914 | get { 915 | paragraphStyle.alignment = NSTextAlignment.natural 916 | return self 917 | } 918 | } 919 | 920 | /** 921 | Set the paragraph line spacing 922 | */ 923 | open func paragraphLineSpacing(_ number: CGFloat) -> Attributer { 924 | paragraphStyle.lineSpacing = number 925 | return self 926 | } 927 | 928 | /** 929 | Set the paragraph spacing 930 | */ 931 | open func paragraphSpacing(_ number: CGFloat) -> Attributer { 932 | paragraphStyle.paragraphSpacing = number 933 | return self 934 | } 935 | 936 | /** 937 | Set the paragraph first line head indent 938 | */ 939 | open func paragraphFirstLineHeadIndent(_ number: CGFloat) -> Attributer { 940 | paragraphStyle.firstLineHeadIndent = number 941 | return self 942 | } 943 | 944 | /** 945 | Set the paragraph head indent 946 | */ 947 | open func paragraphHeadIndent(_ number: CGFloat) -> Attributer { 948 | paragraphStyle.headIndent = number 949 | return self 950 | } 951 | 952 | /** 953 | Set the paragraph tail indent 954 | */ 955 | open func paragraphTailIndent(_ number: CGFloat) -> Attributer { 956 | paragraphStyle.tailIndent = number 957 | return self 958 | } 959 | 960 | /** 961 | Set the paragraph linebreak mode to word wrapping 962 | */ 963 | open var paragraphLineBreakModeWordWrapping: Attributer { 964 | get { 965 | paragraphStyle.lineBreakMode = NSLineBreakMode.byWordWrapping 966 | return self 967 | } 968 | } 969 | 970 | /** 971 | Set the paragraph linebreak mode to character wrapping 972 | */ 973 | open var paragraphLineBreakModeCharWrapping: Attributer { 974 | get { 975 | paragraphStyle.lineBreakMode = NSLineBreakMode.byCharWrapping 976 | return self 977 | } 978 | } 979 | 980 | /** 981 | Set the paragraph linebreak mode to clipping 982 | */ 983 | open var paragraphLineBreakModeClipping: Attributer { 984 | get { 985 | paragraphStyle.lineBreakMode = NSLineBreakMode.byClipping 986 | return self 987 | } 988 | } 989 | 990 | /** 991 | Set the paragraph linebreak mode to truncate head 992 | */ 993 | open var paragraphLineBreakTruncatingHead: Attributer { 994 | get { 995 | paragraphStyle.lineBreakMode = NSLineBreakMode.byTruncatingHead 996 | return self 997 | } 998 | } 999 | 1000 | /** 1001 | Set the paragraph linebreak mode to truncate tail 1002 | */ 1003 | open var paragraphLineBreakTruncatingTail: Attributer { 1004 | get { 1005 | paragraphStyle.lineBreakMode = NSLineBreakMode.byTruncatingTail 1006 | return self 1007 | } 1008 | } 1009 | 1010 | /** 1011 | Set the paragraph linebreak mode to truncate midle 1012 | */ 1013 | open var paragraphLineBreakTruncatingMiddle: Attributer { 1014 | get { 1015 | paragraphStyle.lineBreakMode = NSLineBreakMode.byTruncatingMiddle 1016 | return self 1017 | } 1018 | } 1019 | 1020 | /** 1021 | Set the paragraph minimum line height 1022 | */ 1023 | open func paragraphMinimumLineHeight(_ number: CGFloat) -> Attributer { 1024 | paragraphStyle.minimumLineHeight = number 1025 | return self 1026 | } 1027 | 1028 | /** 1029 | Set the paragraph maximum line height 1030 | */ 1031 | open func paragraphMaximumLineHeight(_ number: CGFloat) -> Attributer { 1032 | paragraphStyle.maximumLineHeight = number 1033 | return self 1034 | } 1035 | 1036 | /** 1037 | Set the paragraph base writing direction to natural 1038 | */ 1039 | open var paragraphBaseWritingDirectionNatural: Attributer { 1040 | get { 1041 | paragraphStyle.baseWritingDirection = NSWritingDirection.natural 1042 | return self 1043 | } 1044 | } 1045 | 1046 | /** 1047 | Set the paragraph base writing direction to left to right 1048 | */ 1049 | open var paragraphBaseWritingDirectionLeftToRight: Attributer { 1050 | get { 1051 | paragraphStyle.baseWritingDirection = NSWritingDirection.leftToRight 1052 | return self 1053 | } 1054 | } 1055 | 1056 | /** 1057 | Set the paragraph base writing direction to right to left 1058 | */ 1059 | open var paragraphBaseWritingDirectionRightToLeft: Attributer { 1060 | get { 1061 | paragraphStyle.baseWritingDirection = NSWritingDirection.rightToLeft 1062 | return self 1063 | } 1064 | } 1065 | 1066 | /** 1067 | Set the paragraph line hight multiple 1068 | */ 1069 | open func paragraphLineHeightMultiple(_ number: CGFloat) -> Attributer { 1070 | paragraphStyle.lineHeightMultiple = number 1071 | return self 1072 | } 1073 | 1074 | /** 1075 | Set the paragraph spacing before 1076 | */ 1077 | open func paragraphSpacingBefore(_ number: CGFloat) -> Attributer { 1078 | paragraphStyle.paragraphSpacingBefore = number 1079 | return self 1080 | } 1081 | 1082 | /** 1083 | Set the paragraph hyphenation factor 1084 | */ 1085 | open func paragraphHyphenationFactor(_ number: Float) -> Attributer { 1086 | paragraphStyle.hyphenationFactor = number 1087 | return self 1088 | } 1089 | 1090 | /* Someone want this? 1091 | @available(tvOS 7.0, *) 1092 | open var tabStops: [NSTextTab]! 1093 | 1094 | @available(tvOS 7.0, *) 1095 | open var defaultTabInterval: CGFloat 1096 | 1097 | @available(tvOS 9.0, *) 1098 | open var allowsDefaultTighteningForTruncation: Bool 1099 | 1100 | 1101 | @available(tvOS 9.0, *) 1102 | open func addTabStop(_ anObject: NSTextTab) 1103 | 1104 | @available(tvOS 9.0, *) 1105 | open func removeTabStop(_ anObject: NSTextTab) 1106 | 1107 | 1108 | @available(tvOS 9.0, *) 1109 | open func setParagraphStyle(_ obj: NSParagraphStyle) 1110 | */ 1111 | 1112 | 1113 | // MARK: - Private 1114 | 1115 | @discardableResult 1116 | fileprivate func applyAttributes(_ attributeName: String, value: AnyObject) -> Attributer { 1117 | for range in self.ranges { 1118 | guard range.location != NSNotFound else { return self } 1119 | self.attributedText.addAttribute(NSAttributedString.Key(rawValue: attributeName), value: value, range: range) 1120 | } 1121 | return self 1122 | } 1123 | 1124 | open func colorFrom(hex: Int) -> UIColor { 1125 | return UIColor(hex: hex) 1126 | } 1127 | 1128 | 1129 | private static var cachedRegularExpressions: [String : NSRegularExpression] = [:] 1130 | private static func regularExpression(for pattern: String) -> NSRegularExpression? { 1131 | if let regex = cachedRegularExpressions[pattern] { 1132 | return regex 1133 | } else if let createdRegex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) { 1134 | cachedRegularExpressions[pattern] = createdRegex 1135 | return createdRegex 1136 | } else { 1137 | return nil 1138 | } 1139 | } 1140 | } 1141 | 1142 | /** 1143 | opperator for easy execution of the .append 1144 | */ 1145 | public func + (left: Attributer, right: Attributer) -> Attributer { 1146 | return left.append(right) 1147 | } 1148 | 1149 | /** 1150 | Helper extension for converting a NSRange to a Range 1151 | */ 1152 | public extension String { 1153 | /** 1154 | Helper extension for converting a NSRange to a Range 1155 | 1156 | -parameter nsRange: The NSRange that needs to be converted 1157 | */ 1158 | func range(from nsRange: NSRange) -> Range? { 1159 | guard 1160 | let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), 1161 | let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex), 1162 | let from = String.Index(from16, within: self), 1163 | let to = String.Index(to16, within: self) 1164 | else { return nil } 1165 | return from ..< to 1166 | } 1167 | } 1168 | 1169 | /** 1170 | Helper extension for creating a UIColor based on a hex value 1171 | */ 1172 | public extension UIColor { 1173 | /** 1174 | Helper extension for creating a UIColor based on a hex value 1175 | 1176 | -parameter hex: The hex value (like 0xffffff) that wil be used for the color 1177 | */ 1178 | convenience init(hex: Int) { 1179 | let red = CGFloat((hex & 0xff0000) >> 16) / 255.0 1180 | let green = CGFloat((hex & 0x00ff00) >> 8) / 255.0 1181 | let blue = CGFloat(hex & 0x0000ff) / 255.0 1182 | self.init(red: red, green: green, blue: blue, alpha: 1.0) 1183 | } 1184 | } 1185 | 1186 | 1187 | -------------------------------------------------------------------------------- /Sources/CGFloatDP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloatDP.swift 3 | // AttributedTextView 4 | // 5 | // Created by Vermeer, Edwin on 05/10/2017. 6 | // Copyright © 2017 evermeer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGFloat { 12 | /** 13 | The relative dimension to the corresponding screen size. 14 | 15 | //Usage 16 | let someView = UIView(frame: CGRect(x: 0, y: 0, width: 320.dp, height: 40.dp) 17 | 18 | **Warning** Only works with size references from @1x mockups. 19 | 20 | The 320 corresponds with your design size. You can change it to: 21 | iPhone 6, 7, 8 Plus — 414 22 | iPhone 6, 7, 8, X — 375 23 | 24 | */ 25 | var dp: CGFloat { 26 | return (self / 320) * UIScreen.main.bounds.width 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Info-tvOS.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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/NSAttributedString+Html.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Html.swift 3 | // AttributedTextView-iOS 4 | // 5 | // Created by Edwin Vermeer on 07-03-18. 6 | // Copyright © 2018 evermeer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | public extension NSMutableAttributedString { 11 | internal convenience init?(html: String) { 12 | guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else { 13 | return nil 14 | } 15 | 16 | guard let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) else { 17 | return nil 18 | } 19 | 20 | self.init(attributedString: attributedString) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/String+Attributer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Attributer.swift 3 | // 4 | // Created by Edwin Vermeer on 25/11/2016. 5 | // Copyright © 2016 Edwin Vermeer. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension String { 11 | 12 | /** 13 | Easy access to the Attributer object 14 | */ 15 | var attributer: Attributer { 16 | get { 17 | return Attributer(self) 18 | } 19 | } 20 | 21 | // All functions below just forward the call to the Attributer 22 | 23 | 24 | // MARK: - Color functions 25 | 26 | /** 27 | Apply the color black to the active range(es) 28 | */ 29 | var black: Attributer { 30 | get { 31 | return attributer.black 32 | } 33 | } 34 | 35 | /** 36 | Apply the color darkGray to the active range(es) 37 | */ 38 | var darkGray: Attributer { 39 | get { 40 | return attributer.darkGray 41 | } 42 | } 43 | 44 | /** 45 | Apply the color lightGray to the active range(es) 46 | */ 47 | var lightGray: Attributer { 48 | get { 49 | return attributer.lightGray 50 | } 51 | } 52 | 53 | /** 54 | Apply the color white to the active range(es) 55 | */ 56 | var white: Attributer { 57 | get { 58 | return attributer.white 59 | } 60 | } 61 | 62 | /** 63 | Apply the color gray to the active range(es) 64 | */ 65 | var gray: Attributer { 66 | get { 67 | return attributer.gray 68 | } 69 | } 70 | 71 | /** 72 | Apply the color red to the active range(es) 73 | */ 74 | var red: Attributer { 75 | get { 76 | return attributer.red 77 | } 78 | } 79 | 80 | /** 81 | Apply the color green to the active range(es) 82 | */ 83 | var green: Attributer { 84 | get { 85 | return attributer.green 86 | } 87 | } 88 | 89 | /** 90 | Apply the color blue to the active range(es) 91 | */ 92 | var blue: Attributer { 93 | get { 94 | return attributer.blue 95 | } 96 | } 97 | 98 | /** 99 | Apply the color cyan to the active range(es) 100 | */ 101 | var cyan: Attributer { 102 | get { 103 | return attributer.cyan 104 | } 105 | } 106 | 107 | /** 108 | Apply the color yellow to the active range(es) 109 | */ 110 | var yellow: Attributer { 111 | get { 112 | return attributer.yellow 113 | } 114 | } 115 | 116 | /** 117 | Apply the color magenta to the active range(es) 118 | */ 119 | var magenta: Attributer { 120 | get { 121 | return attributer.magenta 122 | } 123 | } 124 | 125 | /** 126 | Apply the color orange to the active range(es) 127 | */ 128 | var orange: Attributer { 129 | get { 130 | return attributer.orange 131 | } 132 | } 133 | 134 | /** 135 | Apply the color purple to the active range(es) 136 | */ 137 | var purple: Attributer { 138 | get { 139 | return attributer.purple 140 | } 141 | } 142 | 143 | /** 144 | Apply the color brown to the active range(es) 145 | */ 146 | var brown: Attributer { 147 | get { 148 | return attributer.brown 149 | } 150 | } 151 | 152 | /** 153 | Apply the color clear to the active range(es) 154 | */ 155 | var clear: Attributer { 156 | get { 157 | return attributer.clear 158 | } 159 | } 160 | 161 | /** 162 | Apply a UIColor to the active range(es) 163 | 164 | -parameter color: The UIColor that will be applied. 165 | */ 166 | func color(_ color: UIColor) -> Attributer { 167 | return attributer.color(color) 168 | } 169 | 170 | /** 171 | Apply a color from a hex value to the active range(es) 172 | 173 | -parameter hex: The hex value of the color that will be applied. 174 | */ 175 | func color(_ hex: Int) -> Attributer { 176 | return attributer.color(hex) 177 | } 178 | 179 | // MARK: - Selection functions 180 | 181 | /** 182 | Make the active range the entire text 183 | */ 184 | var all: Attributer { 185 | get { 186 | return attributer.all 187 | } 188 | } 189 | 190 | /** 191 | Make the active range (from and to) 192 | 193 | -parameter from: The start of the new range 194 | -parameter to: The end of the new range. 195 | */ 196 | func range(_ from: Int, to: Int) -> Attributer { 197 | return attributer.range(from, to: to) 198 | } 199 | 200 | /** 201 | Make the active range (location and length) 202 | 203 | -parameter location: The location of the new range 204 | -parameter length: The length of the new range. 205 | */ 206 | func range(_ location: Int, length: Int) -> Attributer { 207 | return attributer.range(location, length: length) 208 | } 209 | 210 | /** 211 | Just set the active range 212 | 213 | -parameter range: The new range. 214 | */ 215 | func range(_ range: NSRange) -> Attributer { 216 | return attributer.range(range) 217 | } 218 | 219 | /** 220 | Find the first occurrance of a string (search with using .CompareOptions) 221 | 222 | -parameter substring: The string to search for. 223 | -parameter options: The search options 224 | */ 225 | func matchWithOptions(_ substring: String, _ options: NSString.CompareOptions = .literal) -> Attributer { 226 | return attributer.matchWithOptions(substring, options) 227 | } 228 | 229 | /** 230 | Find the first occurrance of a string 231 | 232 | -parameter substring: The string to search for. 233 | */ 234 | func match(_ substring: String) -> Attributer { 235 | return attributer.match(substring) 236 | } 237 | 238 | /** 239 | Find the all occurrances of a string (search with using .CompareOptions) 240 | 241 | -parameter substring: The string to search for. 242 | -parameter options: The search options 243 | */ 244 | func matchAllWithOptions(_ substring: String, _ options: NSString.CompareOptions = .literal) -> Attributer { 245 | return attributer.matchAllWithOptions(substring, options) 246 | } 247 | 248 | /** 249 | Find the all occurrances of a string 250 | 251 | -parameter substring: The string to search for. 252 | */ 253 | func matchAll(_ substring: String) -> Attributer { 254 | return attributer.matchAll(substring) 255 | } 256 | 257 | /** 258 | Find the all hashtags (words beginning with #) 259 | */ 260 | var matchHashtags: Attributer { 261 | get { 262 | return attributer.matchHashtags 263 | } 264 | } 265 | 266 | /** 267 | Find the all mentions (words beginning with @) 268 | */ 269 | var matchMentions: Attributer { 270 | get { 271 | return attributer.matchMentions 272 | } 273 | } 274 | 275 | /** 276 | Find the all links 277 | */ 278 | var matchLinks: Attributer { 279 | get { 280 | return attributer.matchLinks 281 | } 282 | } 283 | 284 | /** 285 | Use a regular expression patter to find ranges 286 | 287 | -parameter pattern: The regex pattern. 288 | */ 289 | func matchPattern(_ pattern: String) -> Attributer { 290 | return attributer.matchPattern(pattern) 291 | } 292 | 293 | /** 294 | Append a string to the current attributed text and select the new string as the active range 295 | 296 | -parameter string: The string that will be added 297 | */ 298 | func append(string: String) -> Attributer { 299 | return attributer.append(string) 300 | } 301 | 302 | /** 303 | Append an attributed string to the current attributed text and select the new string as the active range 304 | 305 | -parameter attributedString: The attributed string that will be added 306 | */ 307 | func append(_ attributedString: NSMutableAttributedString) -> Attributer { 308 | return attributer.append(attributedString) 309 | } 310 | 311 | /** 312 | Append an Attributer to the current and select the new string as the active range 313 | 314 | -parameter attributer: The Attributer that will be added 315 | */ 316 | func append(_ attributer: Attributer) -> Attributer { 317 | return attributer.append(attributer) 318 | } 319 | 320 | 321 | // MARK: - Font 322 | 323 | /** 324 | Apply a font to the active range 325 | 326 | -parameter fontName: The name of the font that will be applied 327 | */ 328 | func fontName(_ fontName: String) -> Attributer { 329 | return attributer.fontName(fontName) 330 | } 331 | 332 | /** 333 | Apply a fontsize to the active range 334 | 335 | -parameter size: The fontsize that will be applied 336 | */ 337 | func size(_ size: CGFloat) -> Attributer { 338 | return attributer.size(size) 339 | } 340 | 341 | /** 342 | Apply a font to the active range 343 | 344 | -parameter font: The UIFont that will be applied 345 | */ 346 | func font(_ font: UIFont) -> Attributer { 347 | return attributer.font(font) 348 | } 349 | 350 | 351 | // MARK: - Create a link 352 | 353 | /** 354 | Make the current active range interact. A touch will call the UITextViewDelegate which is handled in the AttributedTextView 355 | 356 | -parameter scheme: The scheme that will be added in front of the link 357 | */ 358 | @available(*, deprecated, message: "use AttributedTextView with makeInteract: instead") 359 | func makeInteractWithURLforScheme(_ scheme: String) -> Attributer { 360 | return attributer.makeInteractWithURLforScheme(scheme) 361 | } 362 | 363 | /** 364 | Make the current active range interact. A touch will call the UITextViewDelegate. When using the AttributedTextView the callback function will be called 365 | 366 | -parameter callback: The callback function that will be called when using AttributedTextView 367 | */ 368 | func makeInteract(_ callback: @escaping ((_ link: String) -> ())) -> Attributer { 369 | return attributer.makeInteract(callback) 370 | } 371 | 372 | /** 373 | Used for setting the link color of the UITextView 374 | 375 | -parameter color: The color of all links 376 | */ 377 | func setLinkColor(_ color: UIColor) -> Attributer { 378 | return attributer.setLinkColor(color) 379 | } 380 | 381 | /** 382 | Called by AttributedTextView when a URL is touched 383 | 384 | -parameter URL: The URL that was touched 385 | */ 386 | func interactWithURL(URL: URL) { 387 | return attributer.interactWithURL(URL: URL) 388 | } 389 | 390 | /** 391 | Make the current selected range a link 392 | 393 | -parameter link: The URL that will be forwarded to the UITextViewDelegate 394 | */ 395 | func link(_ link: URL) -> Attributer { 396 | return attributer.link(link) 397 | } 398 | 399 | /** 400 | Make the current selected range a link 401 | 402 | -parameter link: The NSString that will be forwarded to the UITextViewDelegate 403 | */ 404 | func link(_ link: NSString) -> Attributer { 405 | return attributer.link(link) 406 | } 407 | 408 | // MARK: - Style 409 | 410 | /** 411 | underline the active range 412 | */ 413 | var underline: Attributer { 414 | return attributer.underline 415 | } 416 | 417 | /** 418 | set the underline style for the active range (you also have to call .underline) 419 | 420 | -parameter underline: The underline style 421 | */ 422 | func underline(_ underline: NSUnderlineStyle) -> Attributer { 423 | return attributer.underline(underline) 424 | } 425 | 426 | /** 427 | set the underline style for the active range (you also have to call .underline) 428 | 429 | -parameter style: The underline style 430 | -parameter pattern : The underline pattern 431 | */ 432 | func underline(_ style: NSUnderlineStyle, _ pattern: NSUnderlineStyle) -> Attributer { 433 | return attributer.underline(style, pattern) 434 | } 435 | 436 | /** 437 | set the underline color for the active range (you also have to call .underline) 438 | 439 | -parameter color: the UIColor of the undeline 440 | */ 441 | func underline(_ color: UIColor) -> Attributer { 442 | return attributer.underline(color) 443 | } 444 | 445 | /** 446 | set the underline color for the active range (you also have to call .underline) 447 | 448 | -parameter hex: the hex value of the color for the underline 449 | */ 450 | func underline(_ hex: Int) -> Attributer { 451 | return attributer.underline(hex) 452 | } 453 | 454 | /** 455 | Attach an image to the range 456 | 457 | -parameter image: the UIImage that will be used as the attachment 458 | */ 459 | func attach(_ image: UIImage?, bounds: CGRect? = nil) -> Attributer { 460 | return attributer.attach(image, bounds: bounds) 461 | } 462 | 463 | /** 464 | Attach an image to the range 465 | 466 | -parameter imageStr: the name of the image that will be used as the attachment 467 | */ 468 | func attach(_ imageStr: String, bounds: CGRect? = nil) -> Attributer { 469 | return attributer.attach(imageStr, bounds: bounds) 470 | } 471 | 472 | /** 473 | Set the shadow for the active range(es) 474 | 475 | -parameter shadow: The NSShadow that will be set 476 | */ 477 | func shadow(_ shadow: NSShadow) -> Attributer { 478 | return attributer.shadow(shadow) 479 | } 480 | 481 | /** 482 | Set the shadow for the active range(es) 483 | 484 | -parameter color: The UIColor for the shadow 485 | -parameter offset: The CGSize offset for the shadow 486 | -parameter blurRadius: The blurRadius for the shadow 487 | */ 488 | func shadow(color: UIColor?, offset: CGSize, blurRadius: CGFloat) -> Attributer { 489 | return attributer.shadow(color: color, offset: offset, blurRadius: blurRadius) 490 | } 491 | 492 | /** 493 | Set the baseline offset for the active range(es) 494 | This will move the text up or down 495 | 496 | -parameter offset: The number of pixels that the text will be moved up or down. 497 | */ 498 | func baselineOffset(_ offset: NSNumber) -> Attributer { 499 | return attributer.baselineOffset(offset) 500 | } 501 | 502 | /** 503 | Set the kern for the active range(es) 504 | This will set the spacing between letters 505 | 506 | -parameter number: The number of pixels that will be between the letters. 507 | */ 508 | func kern(_ number: NSNumber) -> Attributer { 509 | return attributer.kern(number) 510 | } 511 | 512 | /** 513 | Set the striketrhough for the active range(es) 514 | 515 | -parameter number: The number of pixels the strikethrough will be high. 516 | */ 517 | func strikethrough(_ number: NSNumber) -> Attributer { 518 | return attributer.strikethrough(number) 519 | } 520 | 521 | 522 | /** 523 | Set the striketrhough color for the active range(es) 524 | 525 | -parameter color: The color of the striketrhough. 526 | */ 527 | func strikethroughColor(_ color: UIColor) -> Attributer { 528 | return attributer.strikethroughColor(color) 529 | } 530 | 531 | /** 532 | Set the stroke color 533 | 534 | -parameter color: The stroke color. 535 | */ 536 | func strokeColor(_ color: UIColor) -> Attributer { 537 | return attributer.strokeColor(color) 538 | } 539 | 540 | /** 541 | Set the stroke width 542 | 543 | -parameter number: The stroke width. 544 | */ 545 | func strokeWidth(_ number: NSNumber) -> Attributer { 546 | return attributer.strokeWidth(number) 547 | } 548 | 549 | /** 550 | Set the style to letterpress 551 | */ 552 | var letterpress: Attributer { 553 | get { 554 | return attributer.letterpress 555 | } 556 | } 557 | 558 | /** 559 | Set the obliqueness 560 | 561 | -parameter number: The obliqueness. 562 | */ 563 | func obliqueness(_ number: NSNumber) -> Attributer { 564 | return attributer.obliqueness(number) 565 | } 566 | 567 | /** 568 | Set the expansion 569 | 570 | -parameter number: The expansion. 571 | */ 572 | func expansion(_ number: NSNumber) -> Attributer { 573 | return attributer.expansion(number) 574 | } 575 | 576 | /* 577 | "In iOS, horizontal text is always used and specifying a different value is undefined." 578 | 579 | public func verticalGlyphForm(_ number: NSNumber) -> Attributer { 580 | return attributer.verticalGlyphForm(number) 581 | } 582 | */ 583 | 584 | /** 585 | Set the backgroundColor 586 | 587 | -parameter color: The color. 588 | */ 589 | func backgroundColor(_ color: UIColor) -> Attributer { 590 | return attributer.backgroundColor(color) 591 | } 592 | 593 | /** 594 | Set the ligature 595 | 596 | -parameter number: The ligature. 597 | */ 598 | func ligature(_ number: NSNumber) -> Attributer { 599 | return attributer.ligature(number) 600 | } 601 | 602 | /** 603 | Set the attachment 604 | 605 | -parameter attachment: The attachment. 606 | */ 607 | func attachment(_ attachment: NSTextAttachment) -> Attributer { 608 | return attributer.attachment(attachment) 609 | } 610 | 611 | /** 612 | Set the writing directions 613 | 614 | -parameter directions: The directions. 615 | */ 616 | func writingDirection(_ directions: [NSNumber]) -> Attributer { 617 | return attributer.writingDirection(directions) 618 | } 619 | 620 | 621 | // MARK: - Paragraph functions 622 | 623 | /** 624 | Set the paragraph 625 | 626 | -parameter paragraph: The paragraph style. 627 | */ 628 | func paragraph(_ paragraph: NSMutableParagraphStyle) -> Attributer { 629 | return attributer.paragraph(paragraph) 630 | } 631 | 632 | /** 633 | Apply the paragraph stylings that have been set by all the paragraph functions 634 | */ 635 | var paragraphApplyStyling: Attributer { 636 | get { 637 | return attributer.paragraphApplyStyling 638 | } 639 | } 640 | 641 | /** 642 | Align the paragraph in the center 643 | */ 644 | var paragraphAlignCenter: Attributer { 645 | get { 646 | return attributer.paragraphAlignCenter 647 | } 648 | } 649 | 650 | /** 651 | Align the paragraph right 652 | */ 653 | var paragraphAlignRight: Attributer { 654 | get { 655 | return attributer.paragraphAlignRight 656 | } 657 | } 658 | 659 | /** 660 | Align the paragraph left 661 | */ 662 | var paragraphAlignLeft: Attributer { 663 | get { 664 | return attributer.paragraphAlignLeft 665 | } 666 | } 667 | 668 | /** 669 | Align the paragraph justified 670 | */ 671 | var paragraphAlignJustified: Attributer { 672 | get { 673 | return attributer.paragraphAlignJustified 674 | } 675 | } 676 | 677 | /** 678 | Align the paragraph Natural 679 | */ 680 | var paragraphAlignNatural: Attributer { 681 | get { 682 | return attributer.paragraphAlignNatural 683 | } 684 | } 685 | 686 | /** 687 | Set the paragraph line spacing 688 | */ 689 | func paragraphLineSpacing(_ number: CGFloat) -> Attributer { 690 | return attributer.paragraphLineSpacing(number) 691 | } 692 | 693 | /** 694 | Set the paragraph spacing 695 | */ 696 | func paragraphSpacing(_ number: CGFloat) -> Attributer { 697 | return attributer.paragraphSpacing(number) 698 | } 699 | 700 | /** 701 | Set the paragraph first line head indent 702 | */ 703 | func paragraphFirstLineHeadIndent(_ number: CGFloat) -> Attributer { 704 | return attributer.paragraphFirstLineHeadIndent(number) 705 | } 706 | 707 | /** 708 | Set the paragraph head indent 709 | */ 710 | func paragraphHeadIndent(_ number: CGFloat) -> Attributer { 711 | return attributer.paragraphHeadIndent(number) 712 | } 713 | 714 | /** 715 | Set the paragraph tail indent 716 | */ 717 | func paragraphTailIndent(_ number: CGFloat) -> Attributer { 718 | return attributer.paragraphTailIndent(number) 719 | } 720 | 721 | /** 722 | Set the paragraph linebreak mode to word wrapping 723 | */ 724 | var paragraphLineBreakModeWordWrapping: Attributer { 725 | get { 726 | return attributer.paragraphLineBreakModeWordWrapping 727 | } 728 | } 729 | 730 | /** 731 | Set the paragraph linebreak mode to character wrapping 732 | */ 733 | var paragraphLineBreakModeCharWrapping: Attributer { 734 | get { 735 | return attributer.paragraphLineBreakModeCharWrapping 736 | } 737 | } 738 | 739 | /** 740 | Set the paragraph linebreak mode to clipping 741 | */ 742 | var paragraphLineBreakModeClipping: Attributer { 743 | get { 744 | return attributer.paragraphLineBreakModeClipping 745 | } 746 | } 747 | 748 | /** 749 | Set the paragraph linebreak mode to truncate head 750 | */ 751 | var paragraphLineBreakTruncatingHead: Attributer { 752 | get { 753 | return attributer.paragraphLineBreakTruncatingHead 754 | } 755 | } 756 | 757 | /** 758 | Set the paragraph linebreak mode to truncate tail 759 | */ 760 | var paragraphLineBreakTruncatingTail: Attributer { 761 | get { 762 | return attributer.paragraphLineBreakTruncatingTail 763 | } 764 | } 765 | 766 | /** 767 | Set the paragraph linebreak mode to truncate midle 768 | */ 769 | var paragraphLineBreakTruncatingMiddle: Attributer { 770 | get { 771 | return attributer.paragraphLineBreakTruncatingMiddle 772 | } 773 | } 774 | 775 | /** 776 | Set the paragraph minimum line height 777 | */ 778 | func paragraphMinimumLineHeight(_ number: CGFloat) -> Attributer { 779 | return attributer.paragraphMinimumLineHeight(number) 780 | } 781 | 782 | /** 783 | Set the paragraph maximum line height 784 | */ 785 | func paragraphMaximumLineHeight(_ number: CGFloat) -> Attributer { 786 | return attributer.paragraphMaximumLineHeight(number) 787 | } 788 | 789 | /** 790 | Set the paragraph base writing direction to natural 791 | */ 792 | var paragraphBaseWritingDirectionNatural: Attributer { 793 | get { 794 | return attributer.paragraphBaseWritingDirectionNatural 795 | } 796 | } 797 | 798 | /** 799 | Set the paragraph base writing direction to left to right 800 | */ 801 | var paragraphBaseWritingDirectionLeftToRight: Attributer { 802 | get { 803 | return attributer.paragraphBaseWritingDirectionLeftToRight 804 | } 805 | } 806 | 807 | /** 808 | Set the paragraph base writing direction to right to left 809 | */ 810 | var paragraphBaseWritingDirectionRightToLeft: Attributer { 811 | get { 812 | return attributer.paragraphBaseWritingDirectionRightToLeft 813 | } 814 | } 815 | 816 | /** 817 | Set the paragraph line hight multiple 818 | */ 819 | func paragraphLineHeightMultiple(_ number: CGFloat) -> Attributer { 820 | return attributer.paragraphLineHeightMultiple(number) 821 | } 822 | 823 | /** 824 | Set the paragraph spacing before 825 | */ 826 | func paragraphSpacingBefore(_ number: CGFloat) -> Attributer { 827 | return attributer.paragraphSpacingBefore(number) 828 | } 829 | 830 | /** 831 | Set the paragraph hyphenation factor 832 | */ 833 | func paragraphHyphenationFactor(_ number: Float) -> Attributer { 834 | return attributer.paragraphHyphenationFactor(number) 835 | } 836 | 837 | var html: Attributer { get { 838 | return Attributer(NSMutableAttributedString(html: self) ?? NSMutableAttributedString()) 839 | } 840 | } 841 | 842 | /* Someone want this? 843 | @available(tvOS 7.0, *) 844 | public var tabStops: [NSTextTab]! 845 | 846 | @available(tvOS 7.0, *) 847 | public var defaultTabInterval: CGFloat 848 | 849 | @available(tvOS 9.0, *) 850 | public var allowsDefaultTighteningForTruncation: Bool 851 | 852 | 853 | @available(tvOS 9.0, *) 854 | public func addTabStop(_ anObject: NSTextTab) 855 | 856 | @available(tvOS 9.0, *) 857 | public func removeTabStop(_ anObject: NSTextTab) 858 | 859 | 860 | @available(tvOS 9.0, *) 861 | public func setParagraphStyle(_ obj: NSParagraphStyle) 862 | */ 863 | } 864 | -------------------------------------------------------------------------------- /Sources/String+NSRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+NSRange.swift 3 | // AttributedTextView-iOS 4 | // 5 | // Created by Edwin Vermeer on 18-06-18. 6 | // Copyright © 2018 evermeer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension String { 12 | func substring(with nsrange: NSRange) -> Substring? { 13 | guard let range = Range(nsrange, in: self) else { return nil } 14 | return self[range] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | carthage update Nimble Quick --no-use-binaries 2 | --------------------------------------------------------------------------------