├── .github └── FUNDING.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Configs ├── SwiftRichString.plist └── SwiftRichStringTests.plist ├── DemoApp ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── Documentation_Assests ├── Logo.sketch ├── image_1.png ├── image_2.png ├── image_3.png ├── image_4.png ├── image_5.png └── image_6.png ├── ExampleMac ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── ExampleMac.entitlements ├── Info.plist └── ViewController.swift ├── ExampleTvOS ├── AppDelegate.swift ├── Assets.xcassets │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Top Shelf Image.imageset │ │ │ └── Contents.json │ ├── Contents.json │ └── Launch Image.launchimage │ │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── ExampleWatchOS Extension ├── Assets.xcassets │ ├── Complication.complicationset │ │ ├── Circular.imageset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Extra Large.imageset │ │ │ └── Contents.json │ │ ├── Modular.imageset │ │ │ └── Contents.json │ │ └── Utilitarian.imageset │ │ │ └── Contents.json │ └── Contents.json ├── ExtensionDelegate.swift ├── Info.plist ├── InterfaceController.swift ├── NotificationController.swift └── PushNotificationPayload.apns ├── ExampleWatchOS ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Interface.storyboard └── Info.plist ├── ExampleiOS ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── therocket.imageset │ │ ├── 816c77975b4ccce940d41933081b19d7.png │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── UIKit+Extensions.swift ├── ViewController.swift └── file.txt ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftRichString │ ├── Attributes │ ├── ColorConvertible.swift │ ├── CommonsAttributes.swift │ ├── DynamicText.swift │ ├── FontConvertible.swift │ ├── FontData.swift │ ├── FontInfoAttribute.swift │ ├── TextTransform.swift │ └── URLRepresentable.swift │ ├── Extensions │ ├── AttributedString+Attachments.swift │ ├── AttributedString+Extension.swift │ ├── AttributedString+FunctionBuilder.swift │ ├── String+Ext.swift │ ├── String+Subscript.swift │ ├── SystemFonts.swift │ └── UIKit+Extras.swift │ ├── Style │ ├── Style.swift │ ├── StyleGroup.swift │ ├── StyleProtocol.swift │ ├── StyleRegEx.swift │ └── StylesManager.swift │ └── Support │ ├── AssociatedValues.swift │ ├── AsyncTextAttachment.swift │ ├── Compatibility.swift │ ├── Extensions.swift │ ├── OrderedDictionary.swift │ ├── XMLDynamicAttributesResolver.swift │ └── XMLStringBuilder.swift ├── SwiftRichString.playground ├── Contents.swift ├── Sources │ └── AttributedStringController.swift └── contents.xcplayground ├── SwiftRichString.podspec ├── SwiftRichString.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── daniele.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ ├── SwiftRichString-iOS.xcscheme │ │ ├── SwiftRichString-macOS.xcscheme │ │ ├── SwiftRichString-tvOS.xcscheme │ │ └── SwiftRichString-watchOS.xcscheme └── xcuserdata │ └── daniele.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── SwiftRichString.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests ├── LinuxMain.swift └── SwiftRichStringTests │ └── SwiftRichStringTests.swift └── banner.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: malcommac 4 | custom: https://www.paypal.com/paypalme2/danielemargutti 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | xcuserdata 6 | *.xcuserstate 7 | *.xctimeline 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 |

2 | SwiftRichString 3 |

4 | 5 | ## CHANGELOG 6 | 7 | * Version **[2.0.1](#201)** 8 | 9 | * Version **[1.1.0](#101)** (for Swift 4) 10 | * Version **[1.0.1](#101)** (for Swift 4) 11 | * Version **[0.9.10](#0910)** (Latest Swift 3.x compatible version) 12 | * Version **[0.9.9](#099)** 13 | * Version **[0.9.8](#097)** 14 | * Version **[0.9.5](#095)** 15 | 16 | 17 | 18 | ## SwiftRichString 2.0.1 19 | --- 20 | - **Release Date**: 2018-05-20 21 | - **Zipped Version**: [Download 2.0.1](https://github.com/malcommac/SwiftRichString/releases/tag/2.0.1) 22 | 23 | This is a total rewrite of the library to simplify and consolidate the APIs functionalities. See documentation in README for detailed informations. 24 | 25 | 26 | 27 | 28 | ## SwiftRichString 1.1.0 29 | --- 30 | - **Release Date**: 2017-12-28 31 | - **Zipped Version**: [Download 1.1.0](https://github.com/malcommac/SwiftRichString/releases/tag/1.1.0) 32 | 33 | * [#26](https://github.com/malcommac/SwiftRichString/issues/26) Fixed an issue when parsing `
` in `MarkupString` class 34 | * [#29](https://github.com/malcommac/SwiftRichString/issues/29) Fixed several warnings coming to Swift 4 35 | 36 |
37 | 38 | ## SwiftRichString 1.0.1 39 | --- 40 | - **Release Date**: 2017-09-14 41 | - **Zipped Version**: [Download 1.0.0](https://github.com/malcommac/SwiftRichString/releases/tag/1.0.0) 42 | 43 | This is the first version compatible with Swift 4. 44 | 45 | 46 | 47 | ## SwiftRichString 0.9.10 48 | --- 49 | - **Release Date**: 2017-09-18 50 | - **Zipped Version**: [Download 0.9.10](https://github.com/malcommac/SwiftRichString/releases/tag/0.9.10) 51 | 52 | Fix minor issue compiling with Xcode 9 and Swift 3.2 53 | 54 | 55 | 56 | ## SwiftRichString 0.9.9 57 | --- 58 | - **Release Date**: 2017-07-06 59 | - **Zipped Version**: [Download 0.9.9](https://github.com/malcommac/SwiftRichString/releases/tag/0.9.8) 60 | 61 | - [#18](https://github.com/malcommac/SwiftRichString/issues/18) Added `renderTags(withStyles:)` func in `String` extension. It will a shortcut to parse an html-tagged string and return the `NSMutableAttributedString` instance. 62 | - [#19](https://github.com/malcommac/SwiftRichString/issues/19) `MarkupString` classes does not `throws` anymore; when parsing fails to invalid strings it will return `nil`. 63 | - [#5](https://github.com/malcommac/SwiftRichString/issues/5) A new function is added to parse multiple regular expressions and apply to each one one or more styles. It's called `func set(regExpStyles: [RegExpPatternStyles], default dStyle: Style? = nil) -> NSMutableAttributedString` and accepts an array of `RegExpPatternStyles` structs (which defines the regexp rule, options and and array of `Style` to apply on match). `default` parameter allows you to set a default style to apply before rules are evaluated. 64 | - [#2](https://github.com/malcommac/SwiftRichString/issues/2) Resolved an issue with CocoaPods 65 | - [#20](https://github.com/malcommac/SwiftRichString/issues/20) Added compatibility with `watchOS`, `tvOS` and `macOS`. 66 | 67 | 68 | 69 | 70 | ## SwiftRichString 0.9.8 71 | --- 72 | - **Release Date**: 2017-04-02 73 | - **Zipped Version**: [Download 0.9.8](https://github.com/malcommac/SwiftRichString/releases/tag/0.9.8) 74 | 75 | - [#4] Added `set(stylesArray:)` with func in `String` extension 76 | 77 | 78 | 79 | ## SwiftRichString 0.9.5 80 | --- 81 | - **Release Date**: 2017-03-01 82 | - **Zipped Version**: [Download 0.9.5](https://github.com/malcommac/SwiftRichString/releases/tag/0.9.5) 83 | 84 | - [#4](https://github.com/malcommac/SwiftRichString/pull/4) Added support for `.writingDirection` 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /Configs/SwiftRichString.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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2018 Daniele Margutti. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Configs/SwiftRichStringTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /DemoApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DemoApp 4 | // 5 | // Created by Daniele Margutti on 21/03/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /DemoApp/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 | } -------------------------------------------------------------------------------- /DemoApp/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 | -------------------------------------------------------------------------------- /DemoApp/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /DemoApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | 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 | -------------------------------------------------------------------------------- /DemoApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DemoApp 4 | // 5 | // Created by Daniele Margutti on 21/03/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet public var label: UILabel? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | // Do any additional setup after loading the view, typically from a nib. 18 | } 19 | 20 | override func didReceiveMemoryWarning() { 21 | super.didReceiveMemoryWarning() 22 | // Dispose of any resources that can be recreated. 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Documentation_Assests/Logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/Logo.sketch -------------------------------------------------------------------------------- /Documentation_Assests/image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_1.png -------------------------------------------------------------------------------- /Documentation_Assests/image_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_2.png -------------------------------------------------------------------------------- /Documentation_Assests/image_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_3.png -------------------------------------------------------------------------------- /Documentation_Assests/image_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_4.png -------------------------------------------------------------------------------- /Documentation_Assests/image_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_5.png -------------------------------------------------------------------------------- /Documentation_Assests/image_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/Documentation_Assests/image_6.png -------------------------------------------------------------------------------- /ExampleMac/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleMac 4 | // 5 | // Created by Daniele Margutti on 05/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /ExampleMac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /ExampleMac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleMac/ExampleMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ExampleMac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2018 SwiftRichString. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /ExampleMac/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ExampleMac 4 | // 5 | // Created by Daniele Margutti on 05/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | override var representedObject: Any? { 14 | didSet { 15 | // Update the view, if already loaded. 16 | } 17 | } 18 | 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /ExampleTvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleTvOS 4 | // 5 | // Created by Daniele Margutti on 20/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "version" : 1, 9 | "author" : "xcode" 10 | } 11 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleTvOS/Assets.xcassets/Launch Image.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "11.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "tv", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "9.0", 15 | "scale" : "1x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ExampleTvOS/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 | -------------------------------------------------------------------------------- /ExampleTvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIMainStoryboardFile 24 | Main 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | 32 | 33 | -------------------------------------------------------------------------------- /ExampleTvOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ExampleTvOS 4 | // 5 | // Created by Daniele Margutti on 20/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">145" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Complication.complicationset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "idiom" : "watch", 5 | "filename" : "Circular.imageset", 6 | "role" : "circular" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "filename" : "Extra Large.imageset", 11 | "role" : "extra-large" 12 | }, 13 | { 14 | "idiom" : "watch", 15 | "filename" : "Modular.imageset", 16 | "role" : "modular" 17 | }, 18 | { 19 | "idiom" : "watch", 20 | "filename" : "Utilitarian.imageset", 21 | "role" : "utilitarian" 22 | } 23 | ], 24 | "info" : { 25 | "version" : 1, 26 | "author" : "xcode" 27 | } 28 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">145" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">145" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x", 6 | "screen-width" : "<=145" 7 | }, 8 | { 9 | "idiom" : "watch", 10 | "scale" : "2x", 11 | "screen-width" : ">145" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleWatchOS Extension/ExtensionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionDelegate.swift 3 | // ExampleWatchOS Extension 4 | // 5 | // Created by Daniele Margutti on 20/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import WatchKit 10 | 11 | class ExtensionDelegate: NSObject, WKExtensionDelegate { 12 | 13 | func applicationDidFinishLaunching() { 14 | // Perform any final initialization of your application. 15 | } 16 | 17 | func applicationDidBecomeActive() { 18 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 19 | } 20 | 21 | func applicationWillResignActive() { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, etc. 24 | } 25 | 26 | func handle(_ backgroundTasks: Set) { 27 | // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. 28 | for task in backgroundTasks { 29 | // Use a switch statement to check the task type 30 | switch task { 31 | case let backgroundTask as WKApplicationRefreshBackgroundTask: 32 | // Be sure to complete the background task once you’re done. 33 | backgroundTask.setTaskCompletedWithSnapshot(false) 34 | case let snapshotTask as WKSnapshotRefreshBackgroundTask: 35 | // Snapshot tasks have a unique completion call, make sure to set your expiration date 36 | snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) 37 | case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: 38 | // Be sure to complete the connectivity task once you’re done. 39 | connectivityTask.setTaskCompletedWithSnapshot(false) 40 | case let urlSessionTask as WKURLSessionRefreshBackgroundTask: 41 | // Be sure to complete the URL session task once you’re done. 42 | urlSessionTask.setTaskCompletedWithSnapshot(false) 43 | default: 44 | // make sure to complete unhandled task types 45 | task.setTaskCompletedWithSnapshot(false) 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ExampleWatchOS Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | ExampleWatchOS Extension 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionAttributes 26 | 27 | WKAppBundleIdentifier 28 | com.swiftrichstring.ExampleiOS.watchkitapp 29 | 30 | NSExtensionPointIdentifier 31 | com.apple.watchkit 32 | 33 | WKExtensionDelegateClassName 34 | $(PRODUCT_MODULE_NAME).ExtensionDelegate 35 | 36 | 37 | -------------------------------------------------------------------------------- /ExampleWatchOS Extension/InterfaceController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceController.swift 3 | // ExampleWatchOS Extension 4 | // 5 | // Created by Daniele Margutti on 20/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import WatchKit 10 | import Foundation 11 | 12 | 13 | class InterfaceController: WKInterfaceController { 14 | 15 | override func awake(withContext context: Any?) { 16 | super.awake(withContext: context) 17 | 18 | // Configure interface objects here. 19 | } 20 | 21 | override func willActivate() { 22 | // This method is called when watch view controller is about to be visible to user 23 | super.willActivate() 24 | } 25 | 26 | override func didDeactivate() { 27 | // This method is called when watch view controller is no longer visible 28 | super.didDeactivate() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ExampleWatchOS Extension/NotificationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationController.swift 3 | // ExampleWatchOS Extension 4 | // 5 | // Created by Daniele Margutti on 20/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import WatchKit 10 | import Foundation 11 | import UserNotifications 12 | 13 | 14 | class NotificationController: WKUserNotificationInterfaceController { 15 | 16 | override init() { 17 | // Initialize variables here. 18 | super.init() 19 | 20 | // Configure interface objects here. 21 | } 22 | 23 | override func willActivate() { 24 | // This method is called when watch view controller is about to be visible to user 25 | super.willActivate() 26 | } 27 | 28 | override func didDeactivate() { 29 | // This method is called when watch view controller is no longer visible 30 | super.didDeactivate() 31 | } 32 | 33 | /* 34 | override func didReceive(_ notification: UNNotification, withCompletion completionHandler: @escaping (WKUserNotificationInterfaceType) -> Swift.Void) { 35 | // This method is called when a notification needs to be presented. 36 | // Implement it if you use a dynamic notification interface. 37 | // Populate your dynamic notification interface as quickly as possible. 38 | // 39 | // After populating your dynamic notification interface call the completion block. 40 | completionHandler(.custom) 41 | } 42 | */ 43 | } 44 | -------------------------------------------------------------------------------- /ExampleWatchOS Extension/PushNotificationPayload.apns: -------------------------------------------------------------------------------- 1 | { 2 | "aps": { 3 | "alert": { 4 | "body": "Test message", 5 | "title": "Optional title" 6 | }, 7 | "category": "myCategory", 8 | "thread-id":"5280" 9 | }, 10 | 11 | "WatchKit Simulator Actions": [ 12 | { 13 | "title": "First Button", 14 | "identifier": "firstButtonAction" 15 | } 16 | ], 17 | 18 | "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App." 19 | } 20 | -------------------------------------------------------------------------------- /ExampleWatchOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "24x24", 5 | "idiom" : "watch", 6 | "scale" : "2x", 7 | "role" : "notificationCenter", 8 | "subtype" : "38mm" 9 | }, 10 | { 11 | "size" : "27.5x27.5", 12 | "idiom" : "watch", 13 | "scale" : "2x", 14 | "role" : "notificationCenter", 15 | "subtype" : "42mm" 16 | }, 17 | { 18 | "size" : "29x29", 19 | "idiom" : "watch", 20 | "role" : "companionSettings", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "size" : "29x29", 25 | "idiom" : "watch", 26 | "role" : "companionSettings", 27 | "scale" : "3x" 28 | }, 29 | { 30 | "size" : "40x40", 31 | "idiom" : "watch", 32 | "scale" : "2x", 33 | "role" : "appLauncher", 34 | "subtype" : "38mm" 35 | }, 36 | { 37 | "size" : "44x44", 38 | "idiom" : "watch", 39 | "scale" : "2x", 40 | "role" : "appLauncher", 41 | "subtype" : "40mm" 42 | }, 43 | { 44 | "size" : "50x50", 45 | "idiom" : "watch", 46 | "scale" : "2x", 47 | "role" : "appLauncher", 48 | "subtype" : "44mm" 49 | }, 50 | { 51 | "size" : "86x86", 52 | "idiom" : "watch", 53 | "scale" : "2x", 54 | "role" : "quickLook", 55 | "subtype" : "38mm" 56 | }, 57 | { 58 | "size" : "98x98", 59 | "idiom" : "watch", 60 | "scale" : "2x", 61 | "role" : "quickLook", 62 | "subtype" : "42mm" 63 | }, 64 | { 65 | "size" : "108x108", 66 | "idiom" : "watch", 67 | "scale" : "2x", 68 | "role" : "quickLook", 69 | "subtype" : "44mm" 70 | }, 71 | { 72 | "idiom" : "watch-marketing", 73 | "size" : "1024x1024", 74 | "scale" : "1x" 75 | } 76 | ], 77 | "info" : { 78 | "version" : 1, 79 | "author" : "xcode" 80 | } 81 | } -------------------------------------------------------------------------------- /ExampleWatchOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleWatchOS/Base.lproj/Interface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ExampleWatchOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | ExampleiOS 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | UISupportedInterfaceOrientations 24 | 25 | UIInterfaceOrientationPortrait 26 | UIInterfaceOrientationPortraitUpsideDown 27 | 28 | WKCompanionAppBundleIdentifier 29 | com.swiftrichstring.ExampleiOS 30 | WKWatchKitApp 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ExampleiOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleiOS 4 | // 5 | // Created by Daniele Margutti on 05/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ExampleiOS/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 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExampleiOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleiOS/Assets.xcassets/therocket.imageset/816c77975b4ccce940d41933081b19d7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/ExampleiOS/Assets.xcassets/therocket.imageset/816c77975b4ccce940d41933081b19d7.png -------------------------------------------------------------------------------- /ExampleiOS/Assets.xcassets/therocket.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "816c77975b4ccce940d41933081b19d7.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExampleiOS/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 | -------------------------------------------------------------------------------- /ExampleiOS/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 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ExampleiOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ExampleiOS/UIKit+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit+Extensions.swift 3 | // ExampleiOS 4 | // 5 | // Created by daniele on 21/12/2019. 6 | // Copyright © 2019 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | public static func randomColors(_ count: Int) -> [UIColor] { 14 | return (0.. UIColor in 15 | randomColor() 16 | } 17 | } 18 | 19 | public static func randomColor() -> UIColor { 20 | let redValue = CGFloat.random(in: 0...1) 21 | let greenValue = CGFloat.random(in: 0...1) 22 | let blueValue = CGFloat.random(in: 0...1) 23 | 24 | let randomColor = UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1.0) 25 | return randomColor 26 | } 27 | 28 | } 29 | 30 | extension UIFont { 31 | 32 | /// Return the same font with given weight. 33 | /// 34 | /// - Parameter weight: weight you want to get 35 | public func withWeight(_ weight: UIFont.Weight) -> UIFont { 36 | let descriptor = fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight]]) 37 | return UIFont(descriptor: descriptor, size: 0) // size 0 means keep the size as it is 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ExampleiOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ExampleiOS 4 | // 5 | // Created by Daniele Margutti on 05/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet public var textView: UITextView? 14 | 15 | let baseFontSize: CGFloat = 16 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // let text = """ 21 | //- Performed! 22 | //""" 23 | // let base = Style { 24 | // $0.font = UIFont.boldSystemFont(ofSize: 14) 25 | // $0.color = UIColor(hexString: "#8E8E8E") 26 | // } 27 | // 28 | // let xmlStyle = StyleXML(base: base) 29 | // xmlStyle.imageProvider = { imageName, attributes in 30 | // fatalError() 31 | // } 32 | // 33 | // self.textView?.attributedText = text.set(style: xmlStyle) 34 | 35 | // return 36 | 37 | // self.textView?.attributedText = "ciao ciao " + AttributedString(image: UIImage(named: "rocket")!, 38 | // bounds: CGRect(x: 0, y: -20, width: 25, height: 25)) + "ciao ciao" 39 | // 40 | // 41 | // return 42 | // 43 | let bodyHTML = try! String(contentsOfFile: Bundle.main.path(forResource: "file", ofType: "txt")!) 44 | 45 | // Create a set of styles 46 | 47 | let headerStyle = Style { 48 | $0.font = UIFont.boldSystemFont(ofSize: self.baseFontSize * 1.15) 49 | $0.lineSpacing = 1 50 | $0.kerning = Kerning.adobe(-20) 51 | } 52 | let boldStyle = Style { 53 | $0.font = UIFont.boldSystemFont(ofSize: self.baseFontSize) 54 | if #available(iOS 11.0, *) { 55 | $0.dynamicText = DynamicText { 56 | $0.style = .body 57 | $0.maximumSize = 35.0 58 | $0.traitCollection = UITraitCollection(userInterfaceIdiom: .phone) 59 | } 60 | } 61 | } 62 | let italicStyle = Style { 63 | $0.font = UIFont.italicSystemFont(ofSize: self.baseFontSize) 64 | } 65 | 66 | let uppercasedRed = Style { 67 | $0.font = UIFont.italicSystemFont(ofSize: self.baseFontSize) 68 | $0.color = UIColor.red 69 | $0.textTransforms = [ 70 | .uppercase 71 | ] 72 | } 73 | 74 | // And a group of them 75 | let styleGroup = StyleGroup(base: Style { 76 | $0.font = UIFont.systemFont(ofSize: self.baseFontSize) 77 | $0.lineSpacing = 2 78 | $0.kerning = Kerning.adobe(-15) 79 | }, [ 80 | "ur": uppercasedRed, 81 | "h3": headerStyle, 82 | "h4": headerStyle, 83 | "h5": headerStyle, 84 | "strong": boldStyle, 85 | "b": boldStyle, 86 | "em": italicStyle, 87 | "i": italicStyle, 88 | "a": uppercasedRed, 89 | "li": Style { 90 | $0.paragraphSpacingBefore = self.baseFontSize / 2 91 | $0.firstLineHeadIndent = self.baseFontSize 92 | $0.headIndent = self.baseFontSize * 1.71 93 | }, 94 | "sup": Style { 95 | $0.font = UIFont.systemFont(ofSize: self.baseFontSize / 1.2) 96 | $0.baselineOffset = Float(self.baseFontSize) / 3.5 97 | }]) 98 | 99 | // Apply a custom xml attribute resolver 100 | styleGroup.xmlAttributesResolver = MyXMLDynamicAttributesResolver() 101 | 102 | // Render 103 | self.textView?.attributedText = bodyHTML.set(style: styleGroup) 104 | 105 | // Accessibility support 106 | if #available(iOS 10.0, *) { 107 | self.textView?.adjustsFontForContentSizeCategory = true 108 | } 109 | 110 | } 111 | } 112 | 113 | public class MyXMLDynamicAttributesResolver: StandardXMLAttributesResolver { 114 | 115 | public override func styleForUnknownXMLTag(_ tag: String, to attributedString: inout AttributedString, attributes: [String : String]?, fromStyle forStyle: StyleXML) { 116 | super.styleForUnknownXMLTag(tag, to: &attributedString, attributes: attributes, fromStyle: forStyle) 117 | 118 | if tag == "rainbow" { 119 | let colors = UIColor.randomColors(attributedString.length) 120 | for i in 0..Parler du don d'organe n'est plus tabou. Je me renseigne, j'en discute avec mes proches,... et je décide! 2 | 3 | 4 | 5 | 6 | 7 | En Belgique, au début des années 2000, le nombre de donneurs d’organes avait tendance à diminuer et les listes d’attente à augmenter considérablement ayant comme corollaire une augmentation de la mortalité des patients inscrits sur les listes d’attente. 8 | 9 | Soucieux de cette situation, en juin 2005, le Ministre en charge de la Santé publique a souhaité mettre sur pied une vaste campagne de sensibilisation entièrement dédiée au don d’organes. 10 | 11 | Depuis, de nombreuses actions entreprises par le SPF Santé publique viennent renforcer toutes celles qui sont accomplies au quotidien par les coordinateurs de transplantation, les coordinateurs locaux de don, les associations de familles de donneurs, les associations de patients transplantés et ce, depuis de très nombreuses années. 12 | 13 | Objectifs poursuivis 14 | 15 | Les objectifs principaux de cette campagne sont: 16 |
  • • d’améliorer la sensibilisation au don auprès des différents groupes auxquels les messages s’adressent,
  • 17 |
  • • d’inviter les citoyens à «oser» en parler en famille, entre amis, entre collègues. Aborder la mort – sa propre mort – reste relativement tabou pour beaucoup.
  • 18 | 19 | Pour en savoir plus...
    www.health.belgium.be/fr/sante/prenez-soin-de-vous/debut-et-fin-de-vie/don-dorganes 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Daniele Margutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftRichString", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftRichString", 12 | targets: ["SwiftRichString"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "SwiftRichString", 23 | dependencies: []), 24 | .testTarget( 25 | name: "SwiftRichStringTests", 26 | dependencies: ["SwiftRichString"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/ColorConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | // MARK: - ColorConvertible 39 | 40 | /// `ColorConvertible` protocol conformance is used to pass your own instance of a color representable object 41 | /// as color's attributes for several properties inside a style. Style get the color instance from your object 42 | /// and use it inside for string attributes. 43 | /// Both `String` and `UIColor`/`NSColor` already conforms this protocol. 44 | public protocol ColorConvertible { 45 | /// Transform a instance of a `ColorConvertible` conform object to a valid `UIColor`/`NSColor`. 46 | var color: Color { get } 47 | } 48 | 49 | // MARK: - ColorConvertible for `UIColor`/`NSColor` 50 | 51 | extension Color: ColorConvertible { 52 | 53 | /// Just return itself 54 | public var color: Color { 55 | return self 56 | } 57 | 58 | /// Initialize a new color from HEX string representation. 59 | /// 60 | /// - Parameter hexString: hex string 61 | public convenience init(hexString: String) { 62 | let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines) 63 | let scanner = Scanner(string: hexString) 64 | 65 | if hexString.hasPrefix("#") { 66 | scanner.scanLocation = 1 67 | } 68 | 69 | var color: UInt32 = 0 70 | 71 | if scanner.scanHexInt32(&color) { 72 | self.init(hex: color, useAlpha: hexString.count > 7) 73 | } else { 74 | self.init(hex: 0x000000) 75 | } 76 | } 77 | 78 | /// Initialize a new color from HEX string as UInt32 with optional alpha chanell. 79 | /// 80 | /// - Parameters: 81 | /// - hex: hex value 82 | /// - alphaChannel: `true` to include alpha channel, `false` to make it opaque. 83 | public convenience init(hex: UInt32, useAlpha alphaChannel: Bool = false) { 84 | let mask = UInt32(0xFF) 85 | 86 | let r = hex >> (alphaChannel ? 24 : 16) & mask 87 | let g = hex >> (alphaChannel ? 16 : 8) & mask 88 | let b = hex >> (alphaChannel ? 8 : 0) & mask 89 | let a = alphaChannel ? hex & mask : 255 90 | 91 | let red = CGFloat(r) / 255 92 | let green = CGFloat(g) / 255 93 | let blue = CGFloat(b) / 255 94 | let alpha = CGFloat(a) / 255 95 | 96 | self.init(red: red, green: green, blue: blue, alpha: alpha) 97 | } 98 | 99 | } 100 | 101 | // MARK: - ColorConvertible for `String` 102 | 103 | extension String: ColorConvertible { 104 | 105 | /// Transform a string to a color. String must be a valid HEX representation of the color. 106 | public var color: Color { 107 | return Color(hexString: self) 108 | } 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/CommonsAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | import CoreGraphics 33 | 34 | //MARK: - Typealiases 35 | 36 | #if os(OSX) 37 | import AppKit 38 | 39 | public typealias Color = NSColor 40 | public typealias Image = NSImage 41 | public typealias Font = NSFont 42 | public typealias FontDescriptor = NSFontDescriptor 43 | public typealias SymbolicTraits = NSFontDescriptor.SymbolicTraits 44 | public typealias LineBreak = NSLineBreakMode 45 | 46 | let FontDescriptorFeatureSettingsAttribute = NSFontDescriptor.AttributeName.featureSettings 47 | let FontFeatureTypeIdentifierKey = NSFontDescriptor.FeatureKey.typeIdentifier 48 | let FontFeatureSelectorIdentifierKey = NSFontDescriptor.FeatureKey.selectorIdentifier 49 | 50 | #else 51 | import UIKit 52 | 53 | public typealias Color = UIColor 54 | public typealias Image = UIImage 55 | public typealias Font = UIFont 56 | public typealias FontDescriptor = UIFontDescriptor 57 | public typealias SymbolicTraits = UIFontDescriptor.SymbolicTraits 58 | public typealias LineBreak = NSLineBreakMode 59 | 60 | let FontDescriptorFeatureSettingsAttribute = UIFontDescriptor.AttributeName.featureSettings 61 | let FontFeatureTypeIdentifierKey = UIFontDescriptor.FeatureKey.featureIdentifier 62 | let FontFeatureSelectorIdentifierKey = UIFontDescriptor.FeatureKey.typeIdentifier 63 | 64 | #endif 65 | 66 | //MARK: - Kerning 67 | 68 | /// An enumeration representing the tracking to be applied. 69 | /// 70 | /// - point: point value. 71 | /// - adobe: adobe format point value. 72 | public enum Kerning { 73 | case point(CGFloat) 74 | case adobe(CGFloat) 75 | 76 | public func kerning(for font: Font?) -> CGFloat { 77 | switch self { 78 | case .point(let kernValue): 79 | return kernValue 80 | case .adobe(let adobeTracking): 81 | let AdobeTrackingDivisor: CGFloat = 1000.0 82 | if font == nil { 83 | print("Missing font for apply tracking; 0 is the fallback.") 84 | } 85 | return (font?.pointSize ?? 0) * (adobeTracking / AdobeTrackingDivisor) 86 | } 87 | } 88 | 89 | } 90 | 91 | //MARK: - Ligatures 92 | 93 | /// Ligatures cause specific character combinations to be rendered using a single custom glyph that corresponds 94 | /// to those characters. 95 | /// 96 | /// - disabled: use only required ligatures when setting text, for the glyphs in the selection 97 | /// if the receiver is a rich text view, or for all glyphs if it’s a plain text view. 98 | /// - defaults: use the standard ligatures available for the fonts and languages used when setting text, 99 | /// for the glyphs in the selection if the receiver is a rich text view, or for all glyphs if it’s a plain text view. 100 | /// - all: use all ligatures available for the fonts and languages used when setting text, for the glyphs 101 | /// in the selection if the receiver is a rich text view, or for all glyphs if it’s a 102 | /// plain text view (not supported on iOS). 103 | public enum Ligatures: Int { 104 | case disabled = 0 105 | case defaults = 1 106 | #if os(OSX) 107 | case all = 2 108 | #endif 109 | } 110 | 111 | //MARK: - HeadingLevel 112 | 113 | /// Specify the heading level of the text. 114 | /// Value is a number in the range 0 to 6. 115 | /// Use 0 to indicate the absence of a specific heading level and use other numbers to indicate the heading level. 116 | /// 117 | /// - none: no heading 118 | /// - one: level 1 119 | /// - two: level 2 120 | /// - three: level 3 121 | /// - four: level 4 122 | /// - five: level 5 123 | /// - six: level 6 124 | public enum HeadingLevel: Int { 125 | case none = 0 126 | case one = 1 127 | case two = 2 128 | case three = 3 129 | case four = 4 130 | case five = 5 131 | case six = 6 132 | } 133 | 134 | //MARK: - Trait Variants 135 | 136 | /// Describe a trait variant for font 137 | public struct TraitVariant: OptionSet { 138 | public var rawValue: Int 139 | 140 | /// The font typestyle is italic 141 | public static let italic = TraitVariant(rawValue: 1 << 0) 142 | 143 | /// The font typestyle is boldface 144 | public static let bold = TraitVariant(rawValue: 1 << 1) 145 | 146 | // The font typestyle is expanded. Expanded and condensed traits are mutually exclusive. 147 | public static let expanded = TraitVariant(rawValue: 1 << 2) 148 | 149 | /// The font typestyle is condensed. Expanded and condensed traits are mutually exclusive 150 | public static let condensed = TraitVariant(rawValue: 1 << 3) 151 | 152 | /// The font uses vertical glyph variants and metrics. 153 | public static let vertical = TraitVariant(rawValue: 1 << 4) 154 | 155 | /// The font synthesizes appropriate attributes for user interface rendering, such as control titles, if necessary. 156 | public static let uiOptimized = TraitVariant(rawValue: 1 << 5) 157 | 158 | /// The font use a tigher line spacing variant. 159 | public static let tightLineSpacing = TraitVariant(rawValue: 1 << 6) 160 | 161 | /// The font use a loose line spacing variant. 162 | public static let looseLineSpacing = TraitVariant(rawValue: 1 << 7) 163 | 164 | public init(rawValue: Int) { 165 | self.rawValue = rawValue 166 | } 167 | } 168 | 169 | 170 | extension TraitVariant { 171 | 172 | var symbolicTraits: SymbolicTraits { 173 | var traits: SymbolicTraits = [] 174 | if contains(.italic) { 175 | traits.insert(SymbolicTraits.italic) 176 | } 177 | if contains(.bold) { 178 | traits.insert(.bold) 179 | } 180 | if contains(.expanded) { 181 | traits.insert(.expanded) 182 | } 183 | if contains(.condensed) { 184 | traits.insert(.condensed) 185 | } 186 | if contains(.vertical) { 187 | traits.insert(.vertical) 188 | } 189 | if contains(.uiOptimized) { 190 | traits.insert(.uiOptimized) 191 | } 192 | if contains(.tightLineSpacing) { 193 | traits.insert(.tightLineSpacing) 194 | } 195 | if contains(.looseLineSpacing) { 196 | traits.insert(.looseLineSpacing) 197 | } 198 | 199 | return traits 200 | } 201 | 202 | } 203 | 204 | //MARK: - Symbolic Traits (UIFontDescriptorSymbolicTraits) Extensions 205 | 206 | extension SymbolicTraits { 207 | #if os(iOS) || os(tvOS) || os(watchOS) 208 | static var italic: SymbolicTraits { 209 | return .traitItalic 210 | } 211 | static var bold: SymbolicTraits { 212 | return .traitBold 213 | } 214 | static var expanded: SymbolicTraits { 215 | return .traitExpanded 216 | } 217 | static var condensed: SymbolicTraits { 218 | return .traitCondensed 219 | } 220 | static var vertical: SymbolicTraits { 221 | return .traitVertical 222 | } 223 | static var uiOptimized: SymbolicTraits { 224 | return .traitUIOptimized 225 | } 226 | static var tightLineSpacing: SymbolicTraits { 227 | return .traitTightLeading 228 | } 229 | static var looseLineSpacing: SymbolicTraits { 230 | return .traitLooseLeading 231 | } 232 | #else 233 | static var uiOptimized: SymbolicTraits { 234 | return .UIOptimized 235 | } 236 | static var tightLineSpacing: SymbolicTraits { 237 | return .tightLeading 238 | } 239 | static var looseLineSpacing: SymbolicTraits { 240 | return .looseLeading 241 | } 242 | #endif 243 | } 244 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/DynamicText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | #if os(tvOS) || os(watchOS) || os(iOS) 32 | import UIKit 33 | 34 | /// DynamicText encapsulate the attributes for fonts to automatically scale to match the current Dynamic Type settings. It uses UIFontMetrics. 35 | public class DynamicText { 36 | 37 | /// Set the dynamic size text style. 38 | /// You can pass any `UIFont.TextStyle` value, if nil UIFontMetrics.default will be used, 39 | /// which uses the body text style. 40 | public var style: UIFont.TextStyle? 41 | 42 | #if os(OSX) || os(iOS) || os(tvOS) 43 | /// The trait collection to use when determining compatibility. The returned 44 | /// font is appropriate for use in an interface that adopts the specified traits. 45 | public var traitCollection: UITraitCollection? 46 | #endif 47 | 48 | /// Set the maximum size 49 | /// allowed for the font/text. Use this value to constrain the font to 50 | /// the specified size when your interface cannot accommodate text that is any larger. 51 | public var maximumSize: CGFloat? 52 | 53 | public typealias InitHandler = ((DynamicText) -> (Void)) 54 | 55 | //MARK: - INIT 56 | 57 | /// Initialize a new dynamic text with optional configuration handler callback. 58 | /// 59 | /// - Parameter handler: configuration handler callback. 60 | public init(_ handler: InitHandler? = nil) { 61 | handler?(self) 62 | } 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/FontConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | #if os(OSX) 34 | import AppKit 35 | #else 36 | import UIKit 37 | #endif 38 | 39 | //MARK: - FontConvertible Protocol 40 | 41 | /// FontConvertible Protocol; you can implement conformance to any object. 42 | /// By defailt both `String` and `UIFont`/`NSFont` are conform to this protocol. 43 | public protocol FontConvertible { 44 | /// Transform the instance of the object to a valid `UIFont`/`NSFont` instance. 45 | /// 46 | /// - Parameter size: optional size of the font. 47 | /// - Returns: valid font instance. 48 | func font(size: CGFloat?) -> Font 49 | } 50 | 51 | 52 | // MARK: - FontConvertible for UIFont/NSFont 53 | extension Font: FontConvertible { 54 | 55 | /// Return the same instance of the font with specified size. 56 | /// 57 | /// - Parameter size: size of the font in points. If size is `nil`, `Font.systemFontSize` is used. 58 | /// - Returns: instance of the font. 59 | public func font(size: CGFloat?) -> Font { 60 | #if os(tvOS) 61 | return Font(name: self.fontName, size: (size ?? TVOS_SYSTEMFONT_SIZE))! 62 | #elseif os(watchOS) 63 | return Font(name: self.fontName, size: (size ?? WATCHOS_SYSTEMFONT_SIZE))! 64 | #elseif os(macOS) 65 | return Font(descriptor: self.fontDescriptor, size: (size ?? Font.systemFontSize))! 66 | #else 67 | return Font(descriptor: self.fontDescriptor, size: (size ?? Font.systemFontSize)) 68 | #endif 69 | } 70 | 71 | } 72 | 73 | // MARK: - FontConvertible for String 74 | extension String: FontConvertible { 75 | 76 | /// Transform a string to a valid `UIFont`/`NSFont` instance. 77 | /// String must contain a valid Postscript font's name. 78 | /// 79 | /// - Parameter size: size of the font. 80 | /// - Returns: instance of the font. 81 | public func font(size: CGFloat?) -> Font { 82 | #if os(tvOS) 83 | return Font(name: self, size: (size ?? TVOS_SYSTEMFONT_SIZE))! 84 | #elseif os(watchOS) 85 | return Font(name: self, size: (size ?? WATCHOS_SYSTEMFONT_SIZE))! 86 | #else 87 | return Font(name: self, size: (size ?? Font.systemFontSize))! 88 | #endif 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/TextTransform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | public enum TextTransform { 34 | public typealias TransformFunction = (String) -> String 35 | 36 | case lowercase 37 | case uppercase 38 | case capitalized 39 | 40 | case lowercaseWithLocale(Locale) 41 | case uppercaseWithLocale(Locale) 42 | case capitalizedWithLocale(Locale) 43 | case custom(TransformFunction) 44 | 45 | var transformer: TransformFunction { 46 | switch self { 47 | case .lowercase: 48 | return { string in 49 | if #available(iOS 9.0, iOSApplicationExtension 9.0, *) { 50 | return string.localizedLowercase 51 | } else { 52 | return string.lowercased(with: Locale.current) 53 | } 54 | } 55 | 56 | case .uppercase: 57 | return { string in 58 | if #available(iOS 9.0, iOSApplicationExtension 9.0, *) { 59 | return string.localizedUppercase 60 | } else { 61 | return string.uppercased(with: Locale.current) 62 | } 63 | } 64 | 65 | case .capitalized: 66 | return { string in 67 | if #available(iOS 9.0, iOSApplicationExtension 9.0, *) { 68 | return string.localizedCapitalized 69 | } else { 70 | return string.capitalized(with: Locale.current) 71 | } 72 | } 73 | 74 | case .lowercaseWithLocale(let locale): 75 | return { string in 76 | string.lowercased(with: locale) 77 | } 78 | 79 | case .uppercaseWithLocale(let locale): 80 | return { string in 81 | string.uppercased(with: locale) 82 | } 83 | 84 | case .capitalizedWithLocale(let locale): 85 | return { string in 86 | string.capitalized(with: locale) 87 | } 88 | 89 | case .custom(let transform): 90 | return transform 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Attributes/URLRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | public protocol URLRepresentable { 34 | var url: URL { get } 35 | } 36 | 37 | extension URL: URLRepresentable { 38 | 39 | public var url: URL { 40 | return self 41 | } 42 | 43 | public init?(string: String?) { 44 | guard let string = string else { 45 | return nil 46 | } 47 | self.init(string: string) 48 | } 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Extensions/AttributedString+Attachments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | #if os(OSX) 34 | import AppKit 35 | #else 36 | import UIKit 37 | #endif 38 | 39 | public extension AttributedString { 40 | 41 | #if os(iOS) 42 | 43 | /// Initialize a new text attachment with a remote image resource. 44 | /// Image will be loaded asynchronously after the text appear inside the control. 45 | /// 46 | /// - Parameters: 47 | /// - imageURL: url of the image. If url is not valid resource will be not downloaded. 48 | /// - bounds: set a non `nil` value to express set the rect of attachment. 49 | convenience init?(imageURL: String?, bounds: String? = nil) { 50 | guard let imageURL = imageURL, let url = URL(string: imageURL) else { 51 | return nil 52 | } 53 | 54 | let attachment = AsyncTextAttachment() 55 | attachment.imageURL = url 56 | 57 | if let bounds = CGRect(string: bounds) { 58 | attachment.bounds = bounds 59 | } 60 | 61 | self.init(attachment: attachment) 62 | } 63 | 64 | #endif 65 | 66 | #if os(iOS) || os(OSX) 67 | 68 | /// Initialize a new text attachment with local image contained into the assets. 69 | /// 70 | /// - Parameters: 71 | /// - imageNamed: name of the image into the assets; if `nil` resource will be not loaded. 72 | /// - bounds: set a non `nil` value to express set the rect of attachment. 73 | convenience init?(imageNamed: String?, bounds: String? = nil) { 74 | guard let imageNamed = imageNamed else { 75 | return nil 76 | } 77 | 78 | let image = Image(named: imageNamed) 79 | self.init(image: image, bounds: bounds) 80 | } 81 | 82 | /// Initialize a new attributed string from an image. 83 | /// 84 | /// - Parameters: 85 | /// - image: image to use. 86 | /// - bounds: location and size of the image, if `nil` the default bounds is applied. 87 | convenience init?(image: Image?, bounds: String? = nil) { 88 | guard let image = image else { 89 | return nil 90 | } 91 | 92 | #if os(OSX) 93 | let attachment = NSTextAttachment(data: image.pngData()!, ofType: "png") 94 | #else 95 | var attachment: NSTextAttachment! 96 | if #available(iOS 13.0, *) { 97 | // Due to a bug (?) in UIKit we should use two methods to allocate the text attachment 98 | // in order to render the image as template or original. If we use the 99 | // NSTextAttachment(image: image) with a .alwaysOriginal rendering mode it will be 100 | // ignored. 101 | if image.renderingMode == .alwaysTemplate { 102 | attachment = NSTextAttachment(image: image) 103 | } else { 104 | attachment = NSTextAttachment() 105 | attachment.image = image.withRenderingMode(.alwaysOriginal) 106 | } 107 | } else { 108 | // It does not work on iOS12, return empty set.s 109 | // attachment = NSTextAttachment(data: image.pngData()!, ofType: "png") 110 | attachment = NSTextAttachment() 111 | attachment.image = image.withRenderingMode(.alwaysOriginal) 112 | } 113 | #endif 114 | 115 | if let boundsRect = CGRect(string: bounds) { 116 | attachment.bounds = boundsRect 117 | } 118 | 119 | self.init(attachment: attachment) 120 | } 121 | 122 | #endif 123 | 124 | } 125 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Extensions/AttributedString+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | //MARK: - AttributedString Extension 39 | 40 | // The following methods are used to alter an existing attributed string with attributes specified by styles. 41 | public extension AttributedString { 42 | 43 | /// Append existing style's attributed, registered in `StyleManager`, to the receiver string (or substring). 44 | /// 45 | /// - Parameters: 46 | /// - style: valid style name registered in `StyleManager`. 47 | /// If invalid, the same instance of the receiver is returned unaltered. 48 | /// - range: range of substring where style is applied, `nil` to use the entire string. 49 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 50 | @discardableResult 51 | func add(style: String, range: NSRange? = nil) -> AttributedString { 52 | guard let style = Styles[style] else { return self } 53 | return style.add(to: self, range: range) 54 | } 55 | 56 | /// Append ordered sequence of styles registered in `StyleManager` to the receiver. 57 | /// Styles are merged in order to produce a single attribute dictionary which is therefore applied to the string. 58 | /// 59 | /// - Parameters: 60 | /// - styles: ordered list of styles to apply. 61 | /// - range: range of substring where style is applied, `nil` to use the entire string. 62 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 63 | @discardableResult 64 | func add(styles: [String], range: NSRange? = nil) -> AttributedString { 65 | guard let styles = Styles[styles] else { return self } 66 | return styles.mergeStyle().set(to: self, range: range) 67 | } 68 | 69 | /// Replace any existing attributed string's style into the receiver/substring of the receiver 70 | /// with the style which has the specified name and is registered into `StyleManager`. 71 | /// 72 | /// - Parameters: 73 | /// - style: style name to apply. Style instance must be registered in `StyleManager` to be applied. 74 | /// - range: range of substring where style is applied, `nil` to use the entire string. 75 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 76 | @discardableResult 77 | func set(style: String, range: NSRange? = nil) -> AttributedString { 78 | guard let style = Styles[style] else { return self } 79 | return style.set(to: self, range: range) 80 | } 81 | 82 | /// Replace any existing attributed string's style into the receiver/substring of the receiver 83 | /// with a style which is an ordered merge of styles passed. 84 | /// Styles are passed as name and you must register them into `StyleManager` before using this function. 85 | /// 86 | /// - Parameters: 87 | /// - styles: styles name to apply. Instances must be registered into `StyleManager`. 88 | /// - range: range of substring where style is applied, `nil` to use the entire string. 89 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 90 | @discardableResult 91 | func set(styles: [String], range: NSRange? = nil) -> AttributedString { 92 | guard let styles = Styles[styles] else { return self } 93 | return styles.mergeStyle().set(to: self, range: range) 94 | } 95 | 96 | /// Append passed style to the receiver. 97 | /// 98 | /// - Parameters: 99 | /// - style: style to apply. 100 | /// - range: range of substring where style is applied, `nil` to use the entire string. 101 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 102 | @discardableResult 103 | func add(style: StyleProtocol, range: NSRange? = nil) -> AttributedString { 104 | return style.add(to: self, range: range) 105 | } 106 | 107 | /// Append passed sequences of styles to the receiver. 108 | /// Sequences are merged in order in a single style's attribute which is therefore applied to the string. 109 | /// 110 | /// - Parameters: 111 | /// - styles: styles to apply, in order. 112 | /// - range: range of substring where style is applied, `nil` to use the entire string. 113 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 114 | @discardableResult 115 | func add(styles: [StyleProtocol], range: NSRange? = nil) -> AttributedString { 116 | return styles.mergeStyle().add(to: self, range: range) 117 | } 118 | 119 | /// Replace the attributes of the string with passed style. 120 | /// 121 | /// - Parameters: 122 | /// - style: style to apply. 123 | /// - range: range of substring where style is applied, `nil` to use the entire string. 124 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 125 | @discardableResult 126 | func set(style: StyleProtocol, range: NSRange? = nil) -> AttributedString { 127 | return style.set(to: self, range: range) 128 | } 129 | 130 | /// Replace the attributes of the string with a style which is an ordered merge of passed 131 | /// styles sequence. 132 | /// 133 | /// - Parameters: 134 | /// - styles: ordered list of styles to apply. 135 | /// - range: range of substring where style is applied, `nil` to use the entire string. 136 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 137 | @discardableResult 138 | func set(styles: [StyleProtocol], range: NSRange? = nil) -> AttributedString { 139 | return styles.mergeStyle().set(to: self, range: range) 140 | } 141 | 142 | /// Remove passed attribute's keys from the receiver. 143 | /// 144 | /// - Parameters: 145 | /// - keys: attribute's keys to remove. 146 | /// - range: range of substring where style will be removed, `nil` to use the entire string. 147 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 148 | @discardableResult 149 | func removeAttributes(_ keys: [NSAttributedString.Key], range: NSRange) -> Self { 150 | keys.forEach { self.removeAttribute($0, range: range) } 151 | return self 152 | } 153 | 154 | /// Remove all keys defined into passed style from the receiver. 155 | /// 156 | /// - Parameter style: style to use. 157 | /// - Returns: same instance of the receiver with - eventually - modified attributes. 158 | func remove(_ style: StyleProtocol) -> Self { 159 | self.removeAttributes(Array(style.attributes.keys), range: NSMakeRange(0, self.length)) 160 | return self 161 | } 162 | 163 | } 164 | 165 | //MARK: - Operations 166 | 167 | /// Merge two attributed string in a single new attributed string. 168 | /// 169 | /// - Parameters: 170 | /// - lhs: attributed string. 171 | /// - rhs: attributed string. 172 | /// - Returns: new attributed string concatenation of two strings. 173 | public func + (lhs: AttributedString, rhs: AttributedString) -> AttributedString { 174 | let final = NSMutableAttributedString(attributedString: lhs) 175 | final.append(rhs) 176 | return final 177 | } 178 | 179 | /// Merge a plain string with an attributed string to produce a new attributed string. 180 | /// 181 | /// - Parameters: 182 | /// - lhs: plain string. 183 | /// - rhs: attributed string. 184 | /// - Returns: new attributed string. 185 | public func + (lhs: String, rhs: AttributedString) -> AttributedString { 186 | let final = NSMutableAttributedString(string: lhs) 187 | final.append(rhs) 188 | return final 189 | } 190 | 191 | /// Merge an attributed string with a plain string to produce a new attributed string. 192 | /// 193 | /// - Parameters: 194 | /// - lhs: attributed string. 195 | /// - rhs: plain string. 196 | /// - Returns: new attributed string. 197 | public func + (lhs: AttributedString, rhs: String) -> AttributedString { 198 | let final = NSMutableAttributedString(attributedString: lhs) 199 | final.append(NSMutableAttributedString(string: rhs)) 200 | return final 201 | } 202 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Extensions/AttributedString+FunctionBuilder.swift: -------------------------------------------------------------------------------- 1 | //// 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | @_functionBuilder 34 | public class AttributedStringBuilder { 35 | public static func buildBlock(_ components: AttributedString...) -> AttributedString { 36 | let result = NSMutableAttributedString(string: "") 37 | 38 | return components.reduce(into: result) { (result, current) in result.append(current) } 39 | } 40 | } 41 | 42 | extension AttributedString { 43 | 44 | public class func composing(@AttributedStringBuilder _ parts: () -> AttributedString) -> AttributedString { 45 | return parts() 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Extensions/String+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | //MARK: - String Extension 39 | 40 | // The following methods are used to produce an attributed string by a plain string. 41 | public extension String { 42 | 43 | //MARK: RENDERING FUNCTIONS 44 | 45 | /// Apply style with given name defines in global `StylesManager` to the receiver string. 46 | /// If required style is not available this function return `nil`. 47 | /// 48 | /// - Parameters: 49 | /// - style: name of style registered in `StylesManager` singleton. 50 | /// - range: range of substring where style is applied, `nil` to use the entire string. 51 | /// - Returns: rendered attributed string, `nil` if style is not registered. 52 | func set(style: String, range: NSRange? = nil) -> AttributedString? { 53 | return StylesManager.shared[style]?.set(to: self, range: range) 54 | } 55 | 56 | /// Apply a sequence of styles defied in global `StylesManager` to the receiver string. 57 | /// Unregistered styles are ignored. 58 | /// Sequence produces a single merge style where the each n+1 element of the sequence may 59 | /// override existing key set by previous applied style. 60 | /// Resulting attributes dictionary is threfore applied to the string. 61 | /// 62 | /// - Parameters: 63 | /// - styles: ordered list of styles name to apply. Styles must be registed in `StylesManager`. 64 | /// - range: range of substring where style is applied, `nil` to use the entire string. 65 | /// - Returns: attributed string, `nil` if all specified styles required are not registered. 66 | func set(styles: [String], range: NSRange? = nil) -> AttributedString? { 67 | return StylesManager.shared[styles]?.mergeStyle().set(to: self, range: range) 68 | } 69 | 70 | /// Apply passed style to the receiver string. 71 | /// 72 | /// - Parameters: 73 | /// - style: style to apply. 74 | /// - range: range of substring where style is applied, `nil` to use the entire string. 75 | /// - Returns: rendered attributed string. 76 | func set(style: StyleProtocol, range: NSRange? = nil) -> AttributedString { 77 | return style.set(to: self, range: range) 78 | } 79 | 80 | /// Apply passed sequence of `StyleProtocol` instances to the receiver. 81 | /// Sequence produces a single merge style where the each n+1 element of the sequence may 82 | /// override existing key set by previous applied style. 83 | /// Resulting attributes dictionary is threfore applied to the string. 84 | /// 85 | /// - Parameters: 86 | /// - styles: ordered list of styles to apply. Styles must be registed in `StylesManager`. 87 | /// - range: range of substring where style is applied, `nil` to use the entire string. 88 | /// - Returns: attributed string. 89 | func set(styles: [StyleProtocol], range: NSRange? = nil) -> AttributedString { 90 | return styles.mergeStyle().set(to: self, range: range) 91 | } 92 | 93 | } 94 | 95 | //MARK: - Operations 96 | 97 | /// Create a new attributed string using + where the left operand is a plain string and left is a style. 98 | /// 99 | /// - Parameters: 100 | /// - lhs: plain string. 101 | /// - rhs: style to apply. 102 | /// - Returns: rendered attributed string instance 103 | public func + (lhs: String, rhs: StyleProtocol) -> AttributedString { 104 | return rhs.set(to: lhs, range: nil) 105 | } 106 | 107 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Extensions/String+Subscript.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | public extension NSRange { 34 | 35 | /// Convert receiver in String's `Range` for given string instance. 36 | /// 37 | /// - Parameter str: source string 38 | /// - Returns: range, `nil` if invalid 39 | func `in`(_ str: String) -> Range? { 40 | return Range(self, in: str) 41 | } 42 | 43 | } 44 | 45 | // MARK: - Subrange Subscript 46 | public extension String { 47 | 48 | /// Convert an NSRange to String range. 49 | /// 50 | /// - Parameter nsRange: origin NSRange instance. 51 | /// - Returns: Range 52 | func rangeFrom(nsRange : NSRange) -> Range? { 53 | return Range(nsRange, in: self) 54 | } 55 | 56 | /// Return substring from receiver starting at given index for a given length. 57 | /// Return `nil` for invalid ranges. 58 | /// 59 | /// 60 | /// - Parameters: 61 | /// - from: starting index. 62 | /// - length: length of string, 63 | /// - Returns: substring 64 | func substring(from: Int?, length: Int) -> String? { 65 | guard length > 0 else { return nil } 66 | let start = from ?? 0 67 | let end = min(count, max(0, start) + length) 68 | guard start < end else { return nil } 69 | return self[start.. Character? { 78 | guard !self.isEmpty, let stringIndex = self.index(startIndex, offsetBy: index, limitedBy: self.index(before: endIndex)) else { return nil } 79 | return self[stringIndex] 80 | } 81 | 82 | 83 | /// Allows to get a substring at specified range. 84 | /// `String[0..<1]` 85 | /// 86 | /// - Parameter value: substring 87 | subscript(range: Range) -> Substring? { 88 | guard let left = offset(by: range.lowerBound) else { return nil } 89 | guard let right = index(left, offsetBy: range.upperBound - range.lowerBound, 90 | limitedBy: endIndex) else { return nil } 91 | return self[left..) -> Substring? { 99 | if value.upperBound < 0 { 100 | guard abs(value.upperBound) <= count else { return nil } 101 | return self[..<(count - abs(value.upperBound))] 102 | } 103 | guard let right = offset(by: value.upperBound) else { return nil } 104 | return self[..) -> Substring? { 112 | if range.upperBound < 0 { 113 | guard abs(range.lowerBound) <= count else { return nil } 114 | return self[(count - abs(range.lowerBound))...] 115 | } 116 | guard let left = offset(by: range.lowerBound) else { return nil } 117 | guard let right = index(left, offsetBy: range.upperBound - range.lowerBound, limitedBy: endIndex) else { return nil } 118 | return self[left...right] 119 | } 120 | 121 | /// Allows to get a substring for a specified partial range. 122 | /// `String[1...]` 123 | /// 124 | /// - Parameter value: substring 125 | subscript(value: PartialRangeFrom) -> Substring? { 126 | guard let left = self.offset(by: value.lowerBound) else { return nil } 127 | return self[left...] 128 | } 129 | 130 | /// Allows to get a substring starting for a given number of characters. 131 | /// `String[...2]` 132 | /// 133 | /// - Parameter value: substring 134 | subscript(value: PartialRangeThrough) -> Substring? { 135 | guard let right = self.offset(by: value.upperBound) else { return nil } 136 | return self[...right] 137 | } 138 | 139 | 140 | internal func offset(by distance: Int) -> String.Index? { 141 | return index(startIndex, offsetBy: distance, limitedBy: endIndex) 142 | } 143 | 144 | /// Return a new string by removing given prefix. 145 | /// 146 | /// - Parameter prefix: prefix to remove. 147 | /// - Returns: new instance of the string without the prefix 148 | func removing(prefix: String) -> String { 149 | if hasPrefix(prefix) { 150 | let start = index(startIndex, offsetBy: prefix.count) 151 | // return substring(from: start) 152 | return self[start...].string 153 | } 154 | return self 155 | } 156 | 157 | func removing(suffix: String) -> String { 158 | if hasSuffix(suffix) { 159 | let end = index(startIndex, offsetBy: self.count-suffix.count) 160 | return self[.. 4.0) 180 | 181 | #if swift(>=4.1) 182 | #else 183 | extension Collection { 184 | func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { 185 | return try flatMap(transform) 186 | } 187 | } 188 | #endif 189 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Style/StyleGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | public typealias StyleGroup = StyleXML 39 | 40 | public class StyleXML: StyleProtocol { 41 | 42 | // The following attributes are ignored for StyleXML because are read from the sub styles. 43 | public var attributes: [NSAttributedString.Key : Any] = [:] 44 | public var fontData: FontData? = nil 45 | public var textTransforms: [TextTransform]? = nil 46 | 47 | /// Ordered dictionary of the styles inside the group 48 | public private(set) var styles: [String : StyleProtocol] 49 | 50 | /// Style to apply as base. If set, before parsing the string style is applied (add/set) 51 | /// to the existing source. 52 | public var baseStyle: StyleProtocol? 53 | 54 | /// XML Parsing options. 55 | /// By default `escapeString` is applied. 56 | public var xmlParsingOptions: XMLParsingOptions = [.escapeString] 57 | 58 | /// Image provider is called to provide custom image when `StyleXML` encounter a `img` tag image. 59 | /// If not implemented the image is automatically searched inside any bundled `xcassets`. 60 | public var imageProvider: ((_ name: String, _ attributes: [String: String]?) -> Image?)? = nil 61 | 62 | /// Dynamic attributes resolver. 63 | /// By default the `StandardXMLAttributesResolver` instance is used. 64 | public var xmlAttributesResolver: XMLDynamicAttributesResolver = StandardXMLAttributesResolver() 65 | 66 | // MARK: - Initialization 67 | 68 | /// Initialize a new `StyleXML` with a dictionary of style and names. 69 | /// Note: Ordered is not guarantee, use `init(_ styles:[(String, StyleProtocol)]` if you 70 | /// need to keep the order of the styles. 71 | /// 72 | /// - Parameters: 73 | /// - base: base style applied to the entire string. 74 | /// - styles: styles dictionary used to map your xml tags to styles definitions. 75 | public init(base: StyleProtocol? = nil, _ styles: [String: StyleProtocol] = [:]) { 76 | self.styles = styles 77 | self.baseStyle = base 78 | } 79 | 80 | // MARK: - Public Methods 81 | 82 | /// Append a new style with given name inside the list. 83 | /// Order is preserved. 84 | /// 85 | /// - Parameters: 86 | /// - style: style you want to add. 87 | /// - name: unique name of the style. 88 | public func add(style: Style, as name: String) { 89 | return self.styles[name] = style 90 | } 91 | 92 | 93 | /// Remove the style with given name. 94 | /// 95 | /// - Parameter name: name of the style to remove. 96 | /// - Returns: removed style instance. 97 | @discardableResult 98 | public func remove(style name: String) -> StyleProtocol? { 99 | return styles.removeValue(forKey: name) 100 | } 101 | 102 | //MARK: - Rendering Methods 103 | 104 | /// Render given source with styles defined inside the receiver. 105 | /// Styles are added as sum to any existing 106 | /// 107 | /// - Parameters: 108 | /// - source: source to render. 109 | /// - range: range of characters to render, `nil` to apply rendering to the entire content. 110 | /// - Returns: attributed string 111 | public func set(to source: String, range: NSRange?) -> AttributedString { 112 | let attributed = NSMutableAttributedString(string: source) 113 | return self.apply(to: attributed, adding: true, range: range) 114 | } 115 | 116 | /// Render given source string by appending parsed styles to the existing attributed string. 117 | /// 118 | /// - Parameters: 119 | /// - source: source attributed string. 120 | /// - range: range of parse. 121 | /// - Returns: same istance of `source` with applied styles. 122 | public func add(to source: AttributedString, range: NSRange?) -> AttributedString { 123 | return self.apply(to: source, adding: true, range: range) 124 | } 125 | 126 | /// Render given source string by replacing existing styles to parsed tags. 127 | /// 128 | /// - Parameters: 129 | /// - source: source attributed string. 130 | /// - range: range of parse. 131 | /// - Returns: same istance of `source` with applied styles. 132 | public func set(to source: AttributedString, range: NSRange?) -> AttributedString { 133 | return self.apply(to: source, adding: false, range: range) 134 | } 135 | 136 | /// Parse tags and render the attributed string with the styles defined into the receiver. 137 | /// 138 | /// - Parameters: 139 | /// - attrStr: source attributed string 140 | /// - adding: `true` to add styles defined to existing styles, `false` to replace any existing style inside tags. 141 | /// - range: range of operation, `nil` for entire string. 142 | /// - Returns: modified attributed string, same instance of the `source`. 143 | public func apply(to attrStr: AttributedString, adding: Bool, range: NSRange?) -> AttributedString { 144 | do { 145 | let xmlParser = XMLStringBuilder(styleXML: self, string: attrStr.string) 146 | return try xmlParser.parse() 147 | } catch { 148 | debugPrint("Failed to generate attributed string from xml: \(error)") 149 | return attrStr 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Style/StyleProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | public typealias AttributedString = NSMutableAttributedString 34 | 35 | public protocol StyleProtocol: class { 36 | 37 | /// Return the attributes of the style in form of dictionary `NSAttributedStringKey`/`Any`. 38 | var attributes: [NSAttributedString.Key : Any] { get } 39 | 40 | /// Font unique attributes dictionary. 41 | var fontData: FontData? { get } 42 | 43 | var textTransforms: [TextTransform]? { get } 44 | 45 | func set(to source: String, range: NSRange?) -> AttributedString 46 | 47 | func add(to source: AttributedString, range: NSRange?) -> AttributedString 48 | 49 | @discardableResult 50 | func set(to source: AttributedString, range: NSRange?) -> AttributedString 51 | 52 | @discardableResult 53 | func remove(from source: AttributedString, range: NSRange?) -> AttributedString 54 | } 55 | 56 | public extension StyleProtocol { 57 | 58 | func set(to source: String, range: NSRange?) -> AttributedString { 59 | let attributedText = NSMutableAttributedString(string: source) 60 | self.fontData?.addAttributes(to: attributedText, range: nil) 61 | attributedText.addAttributes(self.attributes, range: (range ?? NSMakeRange(0, attributedText.length))) 62 | return applyTextTransform(self.textTransforms, to: attributedText) 63 | } 64 | 65 | func add(to source: AttributedString, range: NSRange?) -> AttributedString { 66 | self.fontData?.addAttributes(to: source, range: range) 67 | source.addAttributes(self.attributes, range: (range ?? NSMakeRange(0, source.length))) 68 | return applyTextTransform(self.textTransforms, to: source) 69 | } 70 | 71 | @discardableResult 72 | func set(to source: AttributedString, range: NSRange?) -> AttributedString { 73 | self.fontData?.addAttributes(to: source, range: range) 74 | source.addAttributes(self.attributes, range: (range ?? NSMakeRange(0, source.length))) 75 | return applyTextTransform(self.textTransforms, to: source) 76 | } 77 | 78 | @discardableResult 79 | func remove(from source: AttributedString, range: NSRange?) -> AttributedString { 80 | self.attributes.keys.forEach({ 81 | source.removeAttribute($0, range: (range ?? NSMakeRange(0, source.length))) 82 | }) 83 | return applyTextTransform(self.textTransforms, to: source) 84 | } 85 | 86 | private func applyTextTransform(_ transforms: [TextTransform]?, to string: AttributedString) -> AttributedString { 87 | guard let transforms = self.textTransforms else { 88 | return string 89 | } 90 | 91 | let mutable = string.mutableStringCopy() 92 | let fullRange = NSRange(location: 0, length: mutable.length) 93 | mutable.enumerateAttributes(in: fullRange, options: [], using: { (_, range, _) in 94 | var substring = mutable.attributedSubstring(from: range).string 95 | transforms.forEach { 96 | substring = $0.transformer(substring) 97 | } 98 | mutable.replaceCharacters(in: range, with: substring) 99 | }) 100 | 101 | return mutable 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Style/StyleRegEx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | /// StyleRegEx allows you to define a style which is applied when one or more regular expressions 39 | /// matches are found in source string or attributed string. 40 | public class StyleRegEx: StyleProtocol { 41 | 42 | //MARK: - PROPERTIES 43 | 44 | /// Regular expression 45 | public private(set) var regex: NSRegularExpression 46 | 47 | /// Base style. If set it will be applied in set before any match. 48 | public private(set) var baseStyle: StyleProtocol? 49 | 50 | /// Applied style 51 | private var style: StyleProtocol 52 | 53 | /// Style attributes 54 | public var attributes: [NSAttributedString.Key : Any] { 55 | return self.style.attributes 56 | } 57 | 58 | public var textTransforms: [TextTransform]? 59 | 60 | /// Font attributes 61 | public var fontData: FontData? { 62 | return self.style.fontData 63 | } 64 | 65 | //MARK: - INIT 66 | 67 | /// Initialize a new regular expression style matcher. 68 | /// 69 | /// - Parameters: 70 | /// - base: base style. it will be applied (in set or add) to the entire string before any other operation. 71 | /// - pattern: pattern of regular expression. 72 | /// - options: matching options of the regular expression; if not specified `caseInsensitive` is used. 73 | /// - handler: configuration handler for style. 74 | public init?(base: StyleProtocol? = nil, 75 | pattern: String, options: NSRegularExpression.Options = .caseInsensitive, 76 | handler: @escaping Style.StyleInitHandler) { 77 | do { 78 | self.regex = try NSRegularExpression(pattern: pattern, options: options) 79 | self.baseStyle = base 80 | self.style = Style(handler) 81 | } catch { 82 | return nil 83 | } 84 | } 85 | 86 | //MARK: - METHOD OVERRIDES 87 | 88 | public func set(to source: String, range: NSRange?) -> AttributedString { 89 | let attributed = NSMutableAttributedString(string: source, attributes: (self.baseStyle?.attributes ?? [:])) 90 | return self.applyStyle(to: attributed, add: false, range: range) 91 | } 92 | 93 | public func add(to source: AttributedString, range: NSRange?) -> AttributedString { 94 | if let base = self.baseStyle { 95 | source.addAttributes(base.attributes, range: (range ?? NSMakeRange(0, source.length))) 96 | } 97 | return self.applyStyle(to: source, add: true, range: range) 98 | } 99 | 100 | public func set(to source: AttributedString, range: NSRange?) -> AttributedString { 101 | if let base = self.baseStyle { 102 | source.setAttributes(base.attributes, range: (range ?? NSMakeRange(0, source.length))) 103 | } 104 | return self.applyStyle(to: source, add: false, range: range) 105 | } 106 | 107 | //MARK: - INTERNAL FUNCTIONS 108 | 109 | /// Regular expression matcher. 110 | /// 111 | /// - Parameters: 112 | /// - str: attributed string. 113 | /// - add: `true` to append styles, `false` to replace existing styles. 114 | /// - range: range, `nil` to apply the style to entire string. 115 | /// - Returns: modified attributed string 116 | private func applyStyle(to str: AttributedString, add: Bool, range: NSRange?) -> AttributedString { 117 | let rangeValue = (range ?? NSMakeRange(0, str.length)) 118 | 119 | let matchOpts = NSRegularExpression.MatchingOptions(rawValue: 0) 120 | self.regex.enumerateMatches(in: str.string, options: matchOpts, range: rangeValue) { 121 | (result : NSTextCheckingResult?, _, _) in 122 | if let r = result { 123 | if add { 124 | str.addAttributes(self.attributes, range: r.range) 125 | } else { 126 | str.setAttributes(self.attributes, range: r.range) 127 | } 128 | } 129 | } 130 | 131 | return str 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Style/StylesManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | /// Shortcut to singleton of the `StylesManager` 34 | public let Styles: StylesManager = StylesManager.shared 35 | 36 | /// StylesManager act as a central repository where you can register your own style and use 37 | /// globally in your app. 38 | public class StylesManager { 39 | 40 | /// Singleton instance. 41 | public static let shared: StylesManager = StylesManager() 42 | 43 | /// You can defeer the creation of a style to the first time its required. 44 | /// Implementing this method you will receive a call with the name of the style 45 | /// you are about to provide and you have a chance to make and return it. 46 | /// Once returned the style is automatically cached. 47 | public var onDeferStyle: ((String) -> (StyleProtocol?, Bool))? = nil 48 | 49 | /// Registered styles. 50 | public private(set) var styles: [String: StyleProtocol] = [:] 51 | 52 | /// Register a new style with given name. 53 | /// 54 | /// - Parameters: 55 | /// - name: unique identifier of style. 56 | /// - style: style to register. 57 | public func register(_ name: String, style: StyleProtocol) { 58 | self.styles[name] = style 59 | } 60 | 61 | /// Return a style registered with given name. 62 | /// 63 | /// - Parameter name: name of the style 64 | public subscript(name: String?) -> StyleProtocol? { 65 | guard let name = name else { return nil } 66 | 67 | if let cachedStyle = self.styles[name] { // style is cached 68 | return cachedStyle 69 | } else { 70 | // check if user can provide a deferred creation for this style 71 | if let (deferredStyle,canCache) = self.onDeferStyle?(name) { 72 | // cache if requested 73 | if canCache, let dStyle = deferredStyle { self.styles[name] = dStyle } 74 | return deferredStyle 75 | } 76 | return nil // nothing 77 | } 78 | } 79 | 80 | /// Return a list of styles registered with given names. 81 | /// 82 | /// - Parameter names: array of style's name to get. 83 | public subscript(names: [String]?) -> [StyleProtocol]? { 84 | guard let names = names else { return nil } 85 | return names.compactMap { self.styles[$0] } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Support/AssociatedValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | import ObjectiveC.runtime 33 | 34 | internal func getAssociatedValue(key: String, object: AnyObject) -> T? { 35 | return (objc_getAssociatedObject(object, key.address) as? AssociatedValue)?.value as? T 36 | } 37 | 38 | internal func getAssociatedValue(key: String, object: AnyObject, initialValue: @autoclosure () -> T) -> T { 39 | return getAssociatedValue(key: key, object: object) ?? setAndReturn(initialValue: initialValue(), key: key, object: object) 40 | } 41 | 42 | internal func getAssociatedValue(key: String, object: AnyObject, initialValue: () -> T) -> T { 43 | return getAssociatedValue(key: key, object: object) ?? setAndReturn(initialValue: initialValue(), key: key, object: object) 44 | } 45 | 46 | fileprivate func setAndReturn(initialValue: T, key: String, object: AnyObject) -> T { 47 | set(associatedValue: initialValue, key: key, object: object) 48 | return initialValue 49 | } 50 | 51 | internal func set(associatedValue: T?, key: String, object: AnyObject) { 52 | set(associatedValue: AssociatedValue(associatedValue), key: key, object: object) 53 | } 54 | 55 | internal func set(weakAssociatedValue: T?, key: String, object: AnyObject) { 56 | set(associatedValue: AssociatedValue(weak: weakAssociatedValue), key: key, object: object) 57 | } 58 | 59 | extension String { 60 | 61 | fileprivate var address: UnsafeRawPointer { 62 | return UnsafeRawPointer(bitPattern: abs(hashValue))! 63 | } 64 | 65 | } 66 | 67 | private func set(associatedValue: AssociatedValue, key: String, object: AnyObject) { 68 | objc_setAssociatedObject(object, key.address, associatedValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 69 | } 70 | 71 | private class AssociatedValue { 72 | 73 | weak var _weakValue: AnyObject? 74 | var _value: Any? 75 | 76 | var value: Any? { 77 | return _weakValue ?? _value 78 | } 79 | 80 | init(_ value: Any?) { 81 | _value = value 82 | } 83 | 84 | init(weak: AnyObject?) { 85 | _weakValue = weak 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Support/AsyncTextAttachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | #if os(OSX) 32 | import AppKit 33 | #else 34 | import UIKit 35 | import MobileCoreServices 36 | #endif 37 | 38 | #if os(iOS) 39 | 40 | @objc public protocol AsyncTextAttachmentDelegate 41 | { 42 | /// Called when the image has been loaded 43 | func textAttachmentDidLoadImage(textAttachment: AsyncTextAttachment, displaySizeChanged: Bool) 44 | } 45 | 46 | /// An image text attachment that gets loaded from a remote URL 47 | public class AsyncTextAttachment: NSTextAttachment { 48 | /// Remote URL for the image 49 | public var imageURL: URL? 50 | 51 | /// To specify an absolute display size. 52 | public var displaySize: CGSize? 53 | 54 | /// if determining the display size automatically this can be used to specify a maximum width. If it is not set then the text container's width will be used 55 | public var maximumDisplayWidth: CGFloat? 56 | 57 | /// A delegate to be informed of the finished download 58 | public weak var delegate: AsyncTextAttachmentDelegate? 59 | 60 | /// Remember the text container from delegate message, the current one gets updated after the download 61 | weak var textContainer: NSTextContainer? 62 | 63 | /// The download task to keep track of whether we are already downloading the image 64 | private var downloadTask: URLSessionDataTask! 65 | 66 | /// The size of the downloaded image. Used if we need to determine display size 67 | private var originalImageSize: CGSize? 68 | 69 | /// Designated initializer 70 | public init(imageURL: URL? = nil, delegate: AsyncTextAttachmentDelegate? = nil) { 71 | self.imageURL = imageURL 72 | self.delegate = delegate 73 | 74 | super.init(data: nil, ofType: nil) 75 | } 76 | 77 | required public init?(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | override public var image: UIImage? { 82 | didSet { 83 | originalImageSize = image?.size 84 | } 85 | } 86 | 87 | // MARK: - Helpers 88 | 89 | private func startAsyncImageDownload() { 90 | guard let imageURL = imageURL, contents == nil, downloadTask == nil else { 91 | return 92 | } 93 | 94 | downloadTask = URLSession.shared.dataTask(with: imageURL) { (data, response, error) in 95 | 96 | defer { 97 | // done with the task 98 | self.downloadTask = nil 99 | } 100 | 101 | guard let data = data, error == nil else { 102 | print(error?.localizedDescription as Any) 103 | return 104 | } 105 | 106 | var displaySizeChanged = false 107 | 108 | self.contents = data 109 | 110 | let ext = imageURL.pathExtension as CFString 111 | if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, nil) { 112 | self.fileType = uti.takeRetainedValue() as String 113 | } 114 | 115 | if let image = UIImage(data: data) { 116 | let imageSize = image.size 117 | 118 | if self.displaySize == nil 119 | { 120 | displaySizeChanged = true 121 | } 122 | 123 | self.originalImageSize = imageSize 124 | } 125 | 126 | DispatchQueue.main.async { 127 | // tell layout manager so that it should refresh 128 | if displaySizeChanged { 129 | self.textContainer?.layoutManager?.setNeedsLayout(forAttachment: self) 130 | } else { 131 | self.textContainer?.layoutManager?.setNeedsDisplay(forAttachment: self) 132 | } 133 | 134 | // notify the optional delegate 135 | self.delegate?.textAttachmentDidLoadImage(textAttachment: self, displaySizeChanged: displaySizeChanged) 136 | } 137 | } 138 | 139 | downloadTask.resume() 140 | } 141 | 142 | public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { 143 | if let image = image { return image } 144 | 145 | guard let contents = contents, let image = UIImage(data: contents) else { 146 | // remember reference so that we can update it later 147 | self.textContainer = textContainer 148 | 149 | startAsyncImageDownload() 150 | 151 | return nil 152 | } 153 | 154 | return image 155 | } 156 | 157 | public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { 158 | if let displaySize = displaySize { 159 | return CGRect(origin: CGPoint.zero, size: displaySize) 160 | } 161 | 162 | if let imageSize = originalImageSize { 163 | let maxWidth = maximumDisplayWidth ?? lineFrag.size.width 164 | let factor = maxWidth / imageSize.width 165 | 166 | return CGRect(origin: CGPoint.zero, size:CGSize(width: Int(imageSize.width * factor), height: Int(imageSize.height * factor))) 167 | } 168 | 169 | return CGRect.zero 170 | } 171 | } 172 | 173 | 174 | extension NSLayoutManager 175 | { 176 | /// Determine the character ranges for an attachment 177 | private func rangesForAttachment(attachment: NSTextAttachment) -> [NSRange]? 178 | { 179 | guard let attributedString = self.textStorage else 180 | { 181 | return nil 182 | } 183 | 184 | // find character range for this attachment 185 | let range = NSRange(location: 0, length: attributedString.length) 186 | 187 | var refreshRanges = [NSRange]() 188 | 189 | attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in 190 | 191 | guard let foundAttachment = value as? NSTextAttachment, foundAttachment == attachment else 192 | { 193 | return 194 | } 195 | 196 | // add this range to the refresh ranges 197 | refreshRanges.append(effectiveRange) 198 | } 199 | 200 | if refreshRanges.count == 0 201 | { 202 | return nil 203 | } 204 | 205 | return refreshRanges 206 | } 207 | 208 | /// Trigger a relayout for an attachment 209 | public func setNeedsLayout(forAttachment attachment: NSTextAttachment) 210 | { 211 | guard let ranges = rangesForAttachment(attachment: attachment) else 212 | { 213 | return 214 | } 215 | 216 | // invalidate the display for the corresponding ranges 217 | for range in ranges.reversed() { 218 | self.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) 219 | 220 | // also need to trigger re-display or already visible images might not get updated 221 | self.invalidateDisplay(forCharacterRange: range) 222 | } 223 | } 224 | 225 | /// Trigger a re-display for an attachment 226 | public func setNeedsDisplay(forAttachment attachment: NSTextAttachment) 227 | { 228 | guard let ranges = rangesForAttachment(attachment: attachment) else 229 | { 230 | return 231 | } 232 | 233 | // invalidate the display for the corresponding ranges 234 | for range in ranges.reversed() { 235 | self.invalidateDisplay(forCharacterRange: range) 236 | } 237 | } 238 | } 239 | 240 | #endif 241 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Support/Compatibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | #if os(OSX) 39 | #if swift(>=4.2) 40 | #else 41 | public typealias NSLineBreakMode = NSParagraphStyle.LineBreakMode 42 | #endif 43 | #endif 44 | 45 | // Helper function inserted by Swift 4.2 migrator. 46 | func convertFromNSAttributedStringKey(_ input: NSAttributedString.Key) -> String { 47 | return input.rawValue 48 | } 49 | 50 | 51 | #if swift(>=4.2) 52 | #else 53 | extension NSAttributedString { 54 | public typealias Key = NSAttributedStringKey 55 | } 56 | #endif 57 | 58 | #if os(iOS) || os(tvOS) || os(watchOS) 59 | 60 | extension NSAttributedString.Key { 61 | #if swift(>=4.2) 62 | #else 63 | static let accessibilitySpeechPunctuation = NSAttributedString.Key(UIAccessibilitySpeechAttributePunctuation) 64 | static let accessibilitySpeechLanguage = NSAttributedString.Key(UIAccessibilitySpeechAttributeLanguage) 65 | static let accessibilitySpeechPitch = NSAttributedString.Key(UIAccessibilitySpeechAttributePitch) 66 | 67 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 68 | static let accessibilitySpeechIPANotation = NSAttributedString.Key(UIAccessibilitySpeechAttributeIPANotation) 69 | 70 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 71 | static let accessibilitySpeechQueueAnnouncement = NSAttributedString.Key(UIAccessibilitySpeechAttributeQueueAnnouncement) 72 | 73 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 74 | static let accessibilityTextHeadingLevel = NSAttributedString.Key(UIAccessibilityTextAttributeHeadingLevel) 75 | #endif 76 | } 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Support/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | #if os(OSX) 33 | import AppKit 34 | #else 35 | import UIKit 36 | #endif 37 | 38 | extension String { 39 | 40 | private static let escapeAmpRegExp = try! NSRegularExpression(pattern: "&(?!(#[0-9]{2,4}|[A-z]{2,6});)", options: NSRegularExpression.Options(rawValue: 0)) 41 | 42 | public func escapeWithUnicodeEntities() -> String { 43 | let range = NSRange(location: 0, length: self.count) 44 | return String.escapeAmpRegExp.stringByReplacingMatches(in: self, 45 | options: NSRegularExpression.MatchingOptions(rawValue: 0), 46 | range: range, 47 | withTemplate: "&") 48 | } 49 | 50 | } 51 | 52 | extension NSNumber { 53 | 54 | internal static func from(float: Float?) -> NSNumber? { 55 | guard let float = float else { return nil } 56 | return NSNumber(value: float) 57 | } 58 | 59 | internal static func from(int: Int?) -> NSNumber? { 60 | guard let int = int else { return nil } 61 | return NSNumber(value: int) 62 | } 63 | 64 | internal static func from(underlineStyle: NSUnderlineStyle?) -> NSNumber? { 65 | guard let v = underlineStyle?.rawValue else { return nil } 66 | return NSNumber(value: v) 67 | } 68 | 69 | internal func toUnderlineStyle() -> NSUnderlineStyle? { 70 | return NSUnderlineStyle.init(rawValue: self.intValue) 71 | } 72 | 73 | } 74 | 75 | extension NSAttributedString { 76 | 77 | @nonobjc func mutableStringCopy() -> NSMutableAttributedString { 78 | guard let copy = mutableCopy() as? NSMutableAttributedString else { 79 | fatalError("Failed to mutableCopy() \(self)") 80 | } 81 | return copy 82 | } 83 | 84 | } 85 | 86 | public extension Array where Array.Element == StyleProtocol { 87 | 88 | /// Merge styles from array of `StyleProtocol` elements. 89 | /// Merge is made in order where each n+1 elements may replace existing keys defined by n-1 elements. 90 | /// 91 | /// - Returns: merged style 92 | func mergeStyle() -> Style { 93 | var attributes: [NSAttributedString.Key:Any] = [:] 94 | var textTransforms = [TextTransform]() 95 | self.forEach { 96 | attributes.merge($0.attributes, uniquingKeysWith: { 97 | (_, new) in 98 | return new 99 | }) 100 | textTransforms.append(contentsOf: $0.textTransforms ?? []) 101 | } 102 | return Style(dictionary: attributes, textTransforms: (textTransforms.isEmpty ? nil : textTransforms)) 103 | } 104 | 105 | } 106 | 107 | extension CGRect { 108 | 109 | init?(string: String?) { 110 | guard let string = string else { 111 | return nil 112 | } 113 | 114 | let components: [CGFloat] = string.components(separatedBy: ",").compactMap { 115 | guard let value = Float($0) else { return nil } 116 | return CGFloat(value) 117 | } 118 | 119 | guard components.count == 4 else { 120 | return nil 121 | } 122 | 123 | self = CGRect(x: components[0], 124 | y: components[1], 125 | width: components[2], 126 | height: components[3]) 127 | } 128 | 129 | } 130 | 131 | #if os(OSX) 132 | 133 | public extension NSImage { 134 | 135 | /// PNG data of the image. 136 | func pngData() -> Data? { 137 | self.lockFocus() 138 | let bitmap = NSBitmapImageRep(focusedViewRect: NSRect(x: 0, y: 0, width: size.width, height: size.height)) 139 | let pngData = bitmap!.representation(using: .png, properties: [:]) 140 | self.unlockFocus() 141 | return pngData 142 | } 143 | 144 | } 145 | 146 | #endif 147 | -------------------------------------------------------------------------------- /Sources/SwiftRichString/Support/OrderedDictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichString 3 | // Elegant Strings & Attributed Strings Toolkit for Swift 4 | // 5 | // Created by Daniele Margutti. 6 | // Copyright © 2018 Daniele Margutti. All rights reserved. 7 | // 8 | // Web: http://www.danielemargutti.com 9 | // Email: hello@danielemargutti.com 10 | // Twitter: @danielemargutti 11 | // 12 | // 13 | // Permission is hereby granted, free of charge, to any person obtaining a copy 14 | // of this software and associated documentation files (the "Software"), to deal 15 | // in the Software without restriction, including without limitation the rights 16 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | // copies of the Software, and to permit persons to whom the Software is 18 | // furnished to do so, subject to the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be included in 21 | // all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | // THE SOFTWARE. 30 | 31 | import Foundation 32 | 33 | public struct OrderedDictionary { 34 | var keys = [K]() 35 | var dict = [K:V]() 36 | 37 | public var count: Int { 38 | return self.keys.count 39 | } 40 | 41 | public subscript(key: K) -> V? { 42 | get { 43 | return self.dict[key] 44 | } 45 | set(newValue) { 46 | if newValue == nil { 47 | self.dict.removeValue(forKey:key) 48 | self.keys = self.keys.filter {$0 != key} 49 | } else { 50 | let oldValue = self.dict.updateValue(newValue!, forKey: key) 51 | if oldValue == nil { 52 | self.keys.append(key) 53 | } 54 | } 55 | } 56 | } 57 | 58 | public mutating func remove(key: K) -> V? { 59 | guard let idx = self.keys.firstIndex(of: key) else { return nil } 60 | self.keys.remove(at: idx) 61 | return self.dict.removeValue(forKey: key) 62 | } 63 | } 64 | 65 | extension OrderedDictionary: Sequence { 66 | public func makeIterator() -> AnyIterator { 67 | var counter = 0 68 | return AnyIterator { 69 | guard counter Image? 51 | 52 | /// You are receiving this event when SwiftRichString correctly render an existing tag but the tag 53 | /// contains extra attributes you may want to handle. 54 | /// For example you can pass a specific tag `text/` and you want to override 55 | /// the color with passed value in tags. 56 | /// 57 | /// - Parameters: 58 | /// - attributedString: attributed string. You will receive it after the style is applied. 59 | /// - xmlStyle: xml style information with tag, applied style and the dictionary with extra attributes. 60 | /// - fromStyle: caller instance of `StyleXML. 61 | func applyDynamicAttributes(to attributedString: inout AttributedString, xmlStyle: XMLDynamicStyle, fromStyle: StyleXML) 62 | 63 | /// You will receive this event when SwiftRichString can't found a received style name into provided group tags. 64 | /// You can decide to handle it. The default receiver for example uses the `a` tag to render passed url if `href` 65 | /// attribute is alo present. 66 | /// 67 | /// - Parameters: 68 | /// - tag: tag name received. 69 | /// - attributedString: attributed string received. 70 | /// - attributes: attributes of the tag received. 71 | /// - fromStyle: caller instance of `StyleXML. 72 | func styleForUnknownXMLTag(_ tag: String, to attributedString: inout AttributedString, attributes: [String: String]?, fromStyle: StyleXML) 73 | 74 | } 75 | 76 | extension XMLDynamicAttributesResolver { 77 | 78 | public func image(name: String, attributes: [String: String]?, fromStyle style: StyleXML) -> Image? { 79 | guard let mappedImage = style.imageProvider?(name, attributes) else { 80 | return Image(named: name) // xcassets fallback 81 | } 82 | 83 | // origin xml style contains mapped image. 84 | return mappedImage 85 | } 86 | 87 | } 88 | 89 | // MARK: - StandardXMLAttributesResolver 90 | 91 | open class StandardXMLAttributesResolver: XMLDynamicAttributesResolver { 92 | 93 | public init() {} 94 | 95 | open func applyDynamicAttributes(to attributedString: inout AttributedString, xmlStyle: XMLDynamicStyle, fromStyle: StyleXML) { 96 | let finalStyleToApply = Style() 97 | xmlStyle.enumerateAttributes { key, value in 98 | switch key { 99 | case "color": // color support 100 | finalStyleToApply.color = Color(hexString: value) 101 | 102 | default: break 103 | } 104 | } 105 | self.styleForUnknownXMLTag(xmlStyle.tag, to: &attributedString, attributes: xmlStyle.xmlAttributes, fromStyle: fromStyle) 106 | attributedString.add(style: finalStyleToApply) 107 | } 108 | 109 | open func styleForUnknownXMLTag(_ tag: String, to attributedString: inout AttributedString, attributes: [String: String]?, fromStyle: StyleXML) { 110 | let finalStyleToApply = Style() 111 | switch tag { 112 | case "a": // href support 113 | finalStyleToApply.linkURL = URL(string: attributes?["href"]) 114 | 115 | case "img": 116 | #if os(iOS) 117 | // Remote Image URL support 118 | if let url = attributes?["url"] { 119 | if let image = AttributedString(imageURL: url, bounds: attributes?["rect"]) { 120 | attributedString.append(image) 121 | } 122 | } 123 | #endif 124 | 125 | #if os(iOS) || os(OSX) 126 | // Local Image support 127 | if let imageName = attributes?["named"] { 128 | if let image = image(name: imageName, attributes: attributes, fromStyle: fromStyle), 129 | let imageString = AttributedString(image: image, bounds: attributes?["rect"]) { 130 | attributedString.append(imageString) 131 | } 132 | } 133 | #endif 134 | 135 | default: 136 | break 137 | } 138 | attributedString.add(style: finalStyleToApply) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /SwiftRichString.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import SwiftRichString 4 | import PlaygroundSupport 5 | 6 | // Create your own styles 7 | 8 | let normal = Style { 9 | $0.font = SystemFonts.Helvetica_Light.font(size: 15) 10 | } 11 | 12 | let bold = Style { 13 | $0.font = SystemFonts.Helvetica_Bold.font(size: 20) 14 | $0.color = UIColor.red 15 | $0.backColor = UIColor.yellow 16 | } 17 | 18 | let italic = normal.byAdding { 19 | $0.traitVariants = .italic 20 | } 21 | 22 | // Create a group which contains your style, each identified by a tag. 23 | let myGroup = StyleGroup(base: normal, ["bold": bold, "italic": italic]) 24 | 25 | // Use tags in your plain string 26 | let str = "Hello Daniele!. You're ready to play with us!" 27 | 28 | let attributedStringController = AttributedStringController() 29 | PlaygroundPage.current.liveView = attributedStringController 30 | 31 | attributedStringController.attributedString = str.set(style: myGroup) 32 | 33 | 34 | -------------------------------------------------------------------------------- /SwiftRichString.playground/Sources/AttributedStringController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | public class AttributedStringController: UIViewController { 5 | 6 | public var attributedString: NSAttributedString? { 7 | get { 8 | return textView?.attributedText 9 | } 10 | set { 11 | textView?.attributedText = newValue 12 | } 13 | } 14 | 15 | private var textView: UITextView? 16 | 17 | public override func loadView() { 18 | let view = UIView() 19 | view.backgroundColor = .white 20 | 21 | let textView = UITextView() 22 | textView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 23 | textView.attributedText = attributedString 24 | view.addSubview(textView) 25 | self.textView = textView 26 | 27 | self.view = view 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SwiftRichString.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SwiftRichString.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SwiftRichString" 3 | s.version = "3.7.2" 4 | s.summary = "Elegant Strings & Attributed Strings Toolkit for Swift" 5 | s.description = <<-DESC 6 | SwiftRichString is the best toolkit to work easily with Strings and Attributed Strings. 7 | DESC 8 | s.homepage = "https://github.com/malcommac/SwiftRichString" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "Daniele Margutti" => "hello@danielemargutti.com" } 11 | s.social_media_url = "https://twitter.com/danielemargutti" 12 | s.ios.deployment_target = "9.0" 13 | s.osx.deployment_target = "10.11" 14 | s.watchos.deployment_target = "2.0" 15 | s.tvos.deployment_target = "9.2" 16 | s.source = { :git => "https://github.com/malcommac/SwiftRichString.git", :tag => s.version.to_s } 17 | s.source_files = "Sources/**/*" 18 | s.frameworks = "Foundation" 19 | s.swift_versions = ['5.1'] 20 | end 21 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/SwiftRichString.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcshareddata/xcschemes/SwiftRichString-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcshareddata/xcschemes/SwiftRichString-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcshareddata/xcschemes/SwiftRichString-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcshareddata/xcschemes/SwiftRichString-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 44 | 45 | 51 | 52 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcuserdata/daniele.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftRichString.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ExampleMac.xcscheme 8 | 9 | orderHint 10 | 5 11 | 12 | ExampleMac.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 5 16 | 17 | ExampleTvOS.xcscheme 18 | 19 | orderHint 20 | 6 21 | 22 | ExampleTvOS.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 6 26 | 27 | ExampleWatchOS (Notification).xcscheme 28 | 29 | orderHint 30 | 8 31 | 32 | ExampleWatchOS (Notification).xcscheme_^#shared#^_ 33 | 34 | orderHint 35 | 8 36 | 37 | ExampleWatchOS.xcscheme 38 | 39 | orderHint 40 | 7 41 | 42 | ExampleWatchOS.xcscheme_^#shared#^_ 43 | 44 | orderHint 45 | 7 46 | 47 | ExampleiOS.xcscheme 48 | 49 | orderHint 50 | 4 51 | 52 | ExampleiOS.xcscheme_^#shared#^_ 53 | 54 | orderHint 55 | 4 56 | 57 | SwiftRichString-iOS.xcscheme_^#shared#^_ 58 | 59 | orderHint 60 | 0 61 | 62 | SwiftRichString-macOS.xcscheme_^#shared#^_ 63 | 64 | orderHint 65 | 1 66 | 67 | SwiftRichString-tvOS.xcscheme_^#shared#^_ 68 | 69 | orderHint 70 | 2 71 | 72 | SwiftRichString-watchOS.xcscheme_^#shared#^_ 73 | 74 | orderHint 75 | 3 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /SwiftRichString.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SwiftRichString.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftRichStringTests 3 | 4 | XCTMain([ 5 | testCase(SwiftRichStringTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/SwiftRichStringTests/SwiftRichStringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRichStringTests.swift 3 | // SwiftRichString 4 | // 5 | // Created by Daniele Margutti on 05/05/2018. 6 | // Copyright © 2018 SwiftRichString. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftRichString 12 | 13 | class SwiftRichStringTests: XCTestCase { 14 | func testExample() { 15 | // This is an example of a functional test case. 16 | // Use XCTAssert and related functions to verify your tests produce the correct results. 17 | //// XCTAssertEqual(SwiftRichString().text, "Hello, World!") 18 | } 19 | 20 | static var allTests = [ 21 | ("testExample", testExample), 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/SwiftRichString/e0b72d5c96968d7802856d2be096202c9798e8d1/banner.png --------------------------------------------------------------------------------