├── .circleci └── config.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── BonMot.podspec ├── BonMot.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── AllTheThings.xcscheme │ ├── BonMot-OSX.xcscheme │ ├── BonMot-OSXTests.xcscheme │ ├── BonMot-iOS.xcscheme │ ├── BonMot-iOSTests.xcscheme │ ├── BonMot-tvOS.xcscheme │ ├── BonMot-tvOSTests.xcscheme │ ├── BonMot-watchOS.xcscheme │ └── Example-iOS.xcscheme ├── CONTRIBUTING.md ├── Dangerfile ├── Example-iOS ├── AppDelegate.swift ├── CatalogViewController.swift ├── DemoStrings.swift ├── Example-iOS.entitlements ├── Resources │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── BonMot-logo.imageset │ │ │ ├── BonMot-logo.pdf │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── LaunchImage.launchimage │ │ │ └── Contents.json │ │ ├── Tennis Racket.imageset │ │ │ ├── Contents.json │ │ │ └── Tennis Racket.pdf │ │ ├── barn.imageset │ │ │ ├── Contents.json │ │ │ └── barn.pdf │ │ ├── bee.imageset │ │ │ ├── Contents.json │ │ │ └── bee.pdf │ │ ├── boat.imageset │ │ │ ├── Contents.json │ │ │ └── boat.pdf │ │ ├── bug.imageset │ │ │ ├── Contents.json │ │ │ └── bug.pdf │ │ ├── circuit.imageset │ │ │ ├── Contents.json │ │ │ └── circuit.pdf │ │ ├── cut.imageset │ │ │ ├── Contents.json │ │ │ └── cut.pdf │ │ ├── discount.imageset │ │ │ ├── Contents.json │ │ │ └── discount.pdf │ │ ├── gift.imageset │ │ │ ├── Contents.json │ │ │ └── gift.pdf │ │ ├── knot.imageset │ │ │ ├── Contents.json │ │ │ └── knot.pdf │ │ ├── oar.imageset │ │ │ ├── Contents.json │ │ │ └── oar.pdf │ │ ├── pin.imageset │ │ │ ├── Contents.json │ │ │ └── pin.pdf │ │ └── robot.imageset │ │ │ ├── Contents.json │ │ │ └── robot.pdf │ ├── Info.plist │ ├── Launch Screen.xib │ ├── Main.storyboard │ └── en.lproj │ │ └── InfoPlist.strings └── StyleViewController.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Resources ├── Licenses.txt ├── assets.sketch └── readme-images │ ├── BonMot-logo.ai │ ├── BonMot-logo.png │ ├── bon-mot-style-attributes-inspector.png │ ├── fish-with-black-comma.png │ ├── ios-type-scaling-behavior.png │ ├── label-with-icon.png │ ├── text-alignment-attributes-inspector.png │ ├── text-alignment-identity-inspector.png │ ├── text-alignment.png │ └── wrapped-label-with-icon.png ├── Sources ├── AccessibilityHeadingLevel.swift ├── BonMot.h ├── Compatibility.swift ├── Composable.swift ├── ContextualAlternates.swift ├── Emphasis.swift ├── FontFeatures.swift ├── FontInspector.swift ├── Image+Tinting.swift ├── Info.plist ├── Ligatures.swift ├── MutableCopying.swift ├── NSAttributedString+BonMot.swift ├── NamedStyles.swift ├── Platform.swift ├── Special.swift ├── StringStyle+Part.swift ├── StringStyle.swift ├── StylisticAlternates.swift ├── Tab.swift ├── Tracking.swift ├── Transform.swift ├── UIKit │ ├── AdaptableTextContainer.swift │ ├── AdaptiveStyle.swift │ ├── AdaptiveStyleTransformation.swift │ ├── AttributedStringTransformation.swift │ ├── EmbeddedTransformation.swift │ ├── NSAttributedString+Adaptive.swift │ ├── StyleableUIElement.swift │ ├── Tab+Adaptive.swift │ ├── TextAlignmentConstraint.swift │ ├── Tracking+Adaptive.swift │ ├── UIKit+AdaptableTextContainerSupport.swift │ └── UIKit+Helpers.swift └── XMLBuilder.swift ├── Tests ├── AccessTests.swift ├── AdaptiveStyleTests.swift ├── AssertHelpers.swift ├── AttributedStringStyleTests.swift ├── BONFontBehaviorTests.swift ├── BonMot-OSXTests.xctestplan ├── BonMot-iOSTests.xctestplan ├── BonMot-tvOSTests.xctestplan ├── Compatibility+Tests.swift ├── ComposableTests.swift ├── EmphasisTests.swift ├── FontInspectorTests.swift ├── Helpers.swift ├── ImageTintingTests.swift ├── Info.plist ├── NSAttributedStringDebugTests.swift ├── Resources │ ├── EBGaramond12-Regular.otf │ ├── robot.png │ ├── rz-logo-black.png │ └── rz-logo-red.png ├── TextAlignmentConstraintTests.swift ├── TransformTests.swift ├── UIKitBehaviorTests.swift ├── UIKitBonMotTests.swift └── XMLTagStyleBuilderTests.swift └── fastlane ├── Fastfile ├── Pluginfile ├── README.md └── actions └── xchtmlreport.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | danger: 5 | executor: xcode-12 6 | steps: 7 | - setup 8 | - run: 9 | name: Install xchtmlreport 10 | command: | 11 | curl -O https://raw.githubusercontent.com/TitouanVanBelle/XCTestHTMLReport/develop/xchtmlreport.rb 12 | brew install --HEAD --build-from-source xchtmlreport.rb 13 | - run: 14 | name: Install xcparse 15 | when: always 16 | command: | 17 | brew install chargepoint/xcparse/xcparse 18 | - run: 19 | name: Tests & Code Coverage 20 | when: always 21 | command: | 22 | bundle exec fastlane coverage_all 23 | # Store xcov Code Coverage HTML report artifact 24 | - store_artifacts: 25 | path: build/BonMot-iOS/xcov 26 | destination: xcov 27 | - store_artifacts: 28 | path: build/BonMot-iOS/slather 29 | destination: slather 30 | - store_artifacts: 31 | path: build/BonMot-iOS/scan 32 | destination: scan 33 | - run: 34 | name: Rename CircleCI JUnit XML 35 | when: always 36 | command: | 37 | mkdir -p build/test-results/danger 38 | cp build/BonMot-iOS/scan/BonMot-iOS.xcresult/report.junit build/test-results/danger/results.xml 39 | - store_test_results: 40 | path: build/test-results 41 | # Install SwiftLint only before Danger because otherwise it fails the build 42 | - run: 43 | name: Install SwiftLint 44 | when: always 45 | command: | 46 | brew install swiftlint 47 | - run: 48 | name: Danger 49 | when: always 50 | command: | 51 | if [ -n "$DANGER_GITHUB_API_TOKEN" ]; then bundle exec danger; else echo "Skipping Danger for forked pull request."; fi 52 | - run: 53 | name: Upload to Codecov 54 | when: always 55 | command: bash <(curl -s https://codecov.io/bash) -f build/BonMot-iOS/slather/cobertura.xml -X coveragepy -X gcov -X xcode 56 | 57 | swift-package: 58 | executor: xcode-12 59 | steps: 60 | - setup 61 | - run: swift build 62 | - run: swift test 63 | 64 | lint-pod: 65 | executor: xcode-12 66 | steps: 67 | - setup 68 | - lint-pod 69 | 70 | fastlane-tests: 71 | executor: xcode-12 72 | steps: 73 | - setup 74 | - run: bundle exec fastlane test_all 75 | 76 | fastlane-tests-xcode-13: 77 | executor: xcode-13 78 | steps: 79 | - setup 80 | - run: bundle exec fastlane test_all 81 | 82 | carthage-build: 83 | executor: xcode-12 84 | steps: 85 | - checkout 86 | - run: 87 | name: Update homebrew dependencies 88 | command: brew update 1> /dev/null 2> /dev/null 89 | - run: 90 | name: Update Carthage 91 | command: brew outdated carthage || (brew uninstall carthage --force; brew install carthage --force-bottle) 92 | # Carthage does not work on Xcode 12 https://github.com/Carthage/Carthage/issues/3019 93 | # - run: carthage build --no-skip-current && for platform in Mac iOS tvOS watchOS; do test -d Carthage/Build/${platform}/BonMot.framework || exit 1; done 94 | 95 | deploy-to-cocoapods: 96 | executor: xcode-12 97 | steps: 98 | - setup 99 | - run: bundle exec pod trunk push 100 | 101 | executors: 102 | xcode-12: 103 | macos: 104 | xcode: "12.5.1" 105 | environment: 106 | LC_ALL: en_US.UTF-8 107 | LANG: en_US.UTF-8 108 | HOMEBREW_NO_AUTO_UPDATE: 1 109 | shell: /bin/bash --login -eo pipefail 110 | xcode-13: 111 | macos: 112 | xcode: "13.0.0" 113 | environment: 114 | LC_ALL: en_US.UTF-8 115 | LANG: en_US.UTF-8 116 | HOMEBREW_NO_AUTO_UPDATE: 1 117 | shell: /bin/bash --login -eo pipefail 118 | 119 | commands: 120 | setup: 121 | description: "Shared setup" 122 | steps: 123 | - checkout 124 | - restore-gems 125 | 126 | restore-gems: 127 | description: "Restore Ruby Gems" 128 | steps: 129 | - run: 130 | name: Set Ruby Version 131 | command: echo "ruby-2.5" > ~/.ruby-version 132 | - restore_cache: 133 | key: 1-gems-{{ checksum "Gemfile.lock" }} 134 | - run: bundle check || bundle install --path vendor/bundle 135 | - save_cache: 136 | key: 1-gems-{{ checksum "Gemfile.lock" }} 137 | paths: 138 | - vendor/bundle 139 | 140 | lint-pod: 141 | description: "Lints podspec with specified Swift version" 142 | parameters: 143 | swift-version: 144 | type: string 145 | default: "5.0" 146 | steps: 147 | - run: bundle exec pod lib lint --swift-version=<< parameters.swift-version >> 148 | 149 | workflows: 150 | version: 2 151 | build-test-deploy: 152 | jobs: 153 | - danger: 154 | filters: 155 | tags: 156 | only: /.*/ 157 | - swift-package: 158 | filters: 159 | tags: 160 | only: /.*/ 161 | - fastlane-tests: 162 | filters: 163 | tags: 164 | only: /.*/ 165 | - fastlane-tests-xcode-13: 166 | filters: 167 | tags: 168 | only: /.*/ 169 | - lint-pod: 170 | filters: 171 | tags: 172 | only: /.*/ 173 | - carthage-build: 174 | filters: 175 | tags: 176 | only: /.*/ 177 | - deploy-to-cocoapods: 178 | context: CocoaPods 179 | requires: 180 | - danger 181 | - swift-package 182 | - fastlane-tests 183 | - fastlane-tests-xcode-13 184 | - lint-pod 185 | - carthage-build 186 | filters: 187 | tags: 188 | only: /\d+(\.\d+)*(-.*)*/ 189 | branches: 190 | ignore: /.*/ 191 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/swift,macos,swiftpm,swiftpackagemanager 2 | # Edit at https://www.gitignore.io/?templates=swift,macos,swiftpm,swiftpackagemanager 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Swift ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xccheckout 55 | *.xcscmblueprint 56 | 57 | ## Obj-C/Swift specific 58 | *.hmap 59 | *.ipa 60 | *.dSYM.zip 61 | *.dSYM 62 | 63 | ## Playgrounds 64 | timeline.xctimeline 65 | playground.xcworkspace 66 | 67 | # Swift Package Manager 68 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 69 | # Packages/ 70 | # Package.pins 71 | # Package.resolved 72 | .build/ 73 | 74 | # CocoaPods 75 | # We recommend against adding the Pods directory to your .gitignore. However 76 | # you should judge for yourself, the pros and cons are mentioned at: 77 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 78 | # Pods/ 79 | # Add this line if you want to avoid checking in source code from the Xcode workspace 80 | # *.xcworkspace 81 | 82 | # Carthage 83 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 84 | # Carthage/Checkouts 85 | 86 | Carthage/Build 87 | 88 | # Accio dependency management 89 | Dependencies/ 90 | .accio/ 91 | 92 | # fastlane 93 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 94 | # screenshots whenever they are needed. 95 | # For more information about the recommended setup visit: 96 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 97 | 98 | fastlane/report.xml 99 | fastlane/Preview.html 100 | fastlane/screenshots/**/*.png 101 | fastlane/test_output 102 | 103 | # Code Injection 104 | # After new code Injection tools there's a generated folder /iOSInjectionProject 105 | # https://github.com/johnno1962/injectionforxcode 106 | 107 | iOSInjectionProject/ 108 | 109 | ### SwiftPackageManager ### 110 | Packages 111 | xcuserdata 112 | *.xcodeproj 113 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - cyclomatic_complexity 3 | - file_length 4 | - function_body_length 5 | - identifier_name 6 | - implicit_return # Re-enable when we drop support for Swift 4.2 7 | - large_tuple 8 | - line_length 9 | - nesting 10 | - type_body_length 11 | 12 | statement_position: 13 | statement_mode: uncuddled_else 14 | 15 | opt_in_rules: 16 | - anyobject_protocol 17 | - identical_operands 18 | - implicit_return 19 | - last_where 20 | - legacy_random 21 | - operator_usage_whitespace 22 | - redundant_objc_attribute 23 | - sorted_imports 24 | - static_operator 25 | - toggle_bool 26 | - unused_control_flow_label 27 | - vertical_whitespace 28 | 29 | trailing_comma: 30 | mandatory_comma: true 31 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BonMot.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "BonMot" 3 | s.version = "6.1.3" 4 | s.summary = "Beautiful, easy attributed strings in Swift" 5 | s.swift_versions = ["5.0"] 6 | s.description = <<-DESC 7 | BonMot removes all the mystery from creating beautiful, powerful attributed strings in Swift. 8 | DESC 9 | s.homepage = "https://github.com/Rightpoint/BonMot" 10 | s.license = 'MIT' 11 | s.author = { "Zev Eisenberg" => "zev@zeveisenberg.com" } 12 | s.source = { :git => "https://github.com/Rightpoint/BonMot.git", :tag => s.version.to_s } 13 | # Setting the twitter url is causing builds to fail due to not being able to reach twitter. 14 | # s.social_media_url = 'https://twitter.com/ZevEisenberg' 15 | s.requires_arc = true 16 | 17 | s.ios.deployment_target = '11.0' 18 | s.ios.source_files = 'Sources/**/*.swift' 19 | 20 | s.tvos.deployment_target = '11.0' 21 | s.tvos.source_files = 'Sources/**/*.swift' 22 | 23 | s.osx.deployment_target = '10.11' 24 | s.osx.source_files = 'Sources/*.swift' 25 | 26 | s.watchos.deployment_target = '2.2' 27 | s.watchos.source_files = 'Sources/*.swift' 28 | 29 | end 30 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/AllTheThings.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 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-OSX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 65 | 71 | 72 | 73 | 75 | 76 | 78 | 79 | 81 | 82 | 84 | 85 | 87 | 88 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 104 | 114 | 115 | 121 | 122 | 123 | 124 | 130 | 131 | 137 | 138 | 139 | 140 | 142 | 143 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-OSXTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 18 | 19 | 20 | 21 | 23 | 29 | 30 | 31 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 51 | 52 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 51 | 57 | 58 | 59 | 60 | 61 | 71 | 72 | 78 | 79 | 80 | 81 | 87 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-iOSTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 18 | 19 | 20 | 21 | 23 | 29 | 30 | 31 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 51 | 52 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | 85 | 86 | 92 | 93 | 94 | 95 | 101 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-tvOSTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 18 | 19 | 20 | 21 | 23 | 29 | 30 | 31 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 51 | 52 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/BonMot-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 45 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 81 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /BonMot.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /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/ 76 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | require 'circleci_artifact' 2 | 3 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet. 4 | has_wip_label = github.pr_labels.any? { |label| label.include? "WIP" } 5 | has_wip_title = github.pr_title.include? "[WIP]" 6 | 7 | if has_wip_label || has_wip_title 8 | warn("PR is classed as Work in Progress") 9 | end 10 | 11 | # Warn when there is a big PR. 12 | warn("Big PR") if git.lines_of_code > 500 13 | 14 | # Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title. 15 | if github.pr_body.length < 3 && git.lines_of_code > 10 16 | warn("Please provide a summary in the Pull Request description") 17 | end 18 | 19 | src_root = File.expand_path('../', __FILE__) 20 | 21 | SCHEME = "BonMot-iOS" 22 | 23 | result_bundle_path = "#{src_root}/build/#{SCHEME}/scan/#{SCHEME}.xcresult-coverage" 24 | xccoverage_files = Dir.glob("#{result_bundle_path}/**/action.xccovreport").sort_by { |filename| File.mtime(filename) }.reverse 25 | xccov_file_direct_path = xccoverage_files.first 26 | 27 | xcov.report( 28 | project: "#{src_root}/BonMot.xcodeproj", 29 | scheme: SCHEME, 30 | output_directory: "#{src_root}/build/#{SCHEME}/xcov", 31 | xccov_file_direct_path: xccov_file_direct_path 32 | ) 33 | 34 | ## ** SwiftLint *** 35 | swiftlint.binary_path = "/usr/local/bin/swiftlint" 36 | swiftlint.config_file = "#{src_root}/.swiftlint.yml" 37 | 38 | # Run SwiftLint and warn us if anything fails it 39 | swiftlint.directory = src_root 40 | swiftlint.lint_files inline_mode: true 41 | 42 | # Getting artifact URLs from CircleCI 43 | 44 | # You must set up the CIRCLE_API_TOKEN manually using these instructions 45 | # https://github.com/Rightpoint/ios-template/tree/master/PRODUCTNAME#danger 46 | token = ENV['CIRCLE_API_TOKEN'] 47 | # These are already in the Circle environment 48 | # https://circleci.com/docs/2.0/env-vars/#build-specific-environment-variables 49 | username = ENV['CIRCLE_PROJECT_USERNAME'] 50 | reponame = ENV['CIRCLE_PROJECT_REPONAME'] 51 | build = ENV['CIRCLE_BUILD_NUM'] 52 | 53 | if !(token.nil? or username.nil? or reponame.nil? or build.nil?) 54 | fetcher = CircleciArtifact::Fetcher.new(token: token, username: username, reponame: reponame, build: build) 55 | 56 | xcov = CircleciArtifact::Query.new(url_substring: 'xcov/index.html') 57 | slather = CircleciArtifact::Query.new(url_substring: 'slather/index.html') 58 | xcpretty = CircleciArtifact::Query.new(url_substring: 'scan/report.html') 59 | xchtmlreport = CircleciArtifact::Query.new(url_substring: 'scan/index.html') 60 | queries = [xcov, slather, xcpretty, xchtmlreport] 61 | results = fetcher.fetch_queries(queries) 62 | 63 | xcov_url = results.url_for_query(xcov) 64 | slather_url = results.url_for_query(slather) 65 | xcpretty_url = results.url_for_query(xcpretty) 66 | xchtmlreport_url = results.url_for_query(xchtmlreport) 67 | 68 | if !xchtmlreport_url.nil? 69 | message "[Test Results](#{xchtmlreport_url})" 70 | else 71 | message "Tests in progress..." 72 | end 73 | 74 | if !slather_url.nil? 75 | message "[Code Coverage](#{slather_url})" 76 | end 77 | else 78 | warn "Missing CircleCI artifacts. Most likely the [CIRCLE_API_TOKEN](https://github.com/Rightpoint/circleci_artifact#getting-started) is not set, or Danger is not running on CircleCI." 79 | end 80 | 81 | # Test Reporting 82 | 83 | junit.parse "#{src_root}/build/BonMot-iOS/scan/BonMot-iOS.xcresult/report.junit" 84 | junit.report 85 | -------------------------------------------------------------------------------- /Example-iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 7/20/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 18 | style() 19 | window?.makeKeyAndVisible() 20 | application.enableAdaptiveContentSizeMonitor() 21 | return true 22 | } 23 | 24 | func style() { 25 | guard let traitCollection = window?.traitCollection else { 26 | fatalError("There should be a traitCollection available before calling this method.") 27 | } 28 | let titleStyle = StringStyle( 29 | .font(UIFont.appFont(ofSize: 20)), 30 | .adapt(.control) 31 | ) 32 | UINavigationBar.appearance().titleTextAttributes = titleStyle.attributes(adaptedTo: traitCollection) 33 | let barStyle = StringStyle( 34 | .font(UIFont.appFont(ofSize: 17)), 35 | .adapt(.control) 36 | ) 37 | UIBarButtonItem.appearance().setTitleTextAttributes(barStyle.attributes(adaptedTo: traitCollection), for: .normal) 38 | } 39 | 40 | } 41 | 42 | extension UIColor { 43 | static var raizlabsRed: UIColor { 44 | return UIColor(hex: 0xEC594D) 45 | } 46 | 47 | convenience init(hex: UInt32, alpha: CGFloat = 1) { 48 | self.init( 49 | red: CGFloat((hex >> 16) & 0xff) / 255.0, 50 | green: CGFloat((hex >> 8) & 0xff) / 255.0, 51 | blue: CGFloat(hex & 0xff) / 255.0, 52 | alpha: alpha) 53 | } 54 | 55 | } 56 | 57 | extension UIFont { 58 | 59 | static func appFont(ofSize pointSize: CGFloat) -> UIFont { 60 | return UIFont(name: "Avenir-Roman", size: pointSize)! 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Example-iOS/CatalogViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CatalogViewController.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 7/27/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CatalogViewController: UIViewController { 12 | @IBAction func displayAlert() { 13 | let controller = UIAlertController(title: "Alert", message: "This is a message", preferredStyle: .alert) 14 | controller.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) 15 | present(controller, animated: true, completion: nil) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example-iOS/Example-iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/BonMot-logo.imageset/BonMot-logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/BonMot-logo.imageset/BonMot-logo.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/BonMot-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "BonMot-logo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "extent" : "full-screen", 14 | "minimum-system-version" : "7.0", 15 | "subtype" : "retina4", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/Tennis Racket.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Tennis Racket.pdf", 6 | "alignment-insets" : { 7 | "top" : 0, 8 | "left" : 0, 9 | "bottom" : -10, 10 | "right" : 0 11 | } 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/Tennis Racket.imageset/Tennis Racket.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/Tennis Racket.imageset/Tennis Racket.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/barn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "barn.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/barn.imageset/barn.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/barn.imageset/barn.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/bee.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bee.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/bee.imageset/bee.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/bee.imageset/bee.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/boat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "boat.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/boat.imageset/boat.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/boat.imageset/boat.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/bug.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bug.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/bug.imageset/bug.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/bug.imageset/bug.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/circuit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "circuit.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/circuit.imageset/circuit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/circuit.imageset/circuit.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/cut.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cut.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/cut.imageset/cut.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/cut.imageset/cut.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/discount.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "discount.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/discount.imageset/discount.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/discount.imageset/discount.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/gift.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "gift.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/gift.imageset/gift.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/gift.imageset/gift.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/knot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "knot.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/knot.imageset/knot.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/knot.imageset/knot.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/oar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "oar.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/oar.imageset/oar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/oar.imageset/oar.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/pin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pin.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/pin.imageset/pin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/pin.imageset/pin.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/robot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "robot.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example-iOS/Resources/Images.xcassets/robot.imageset/robot.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Example-iOS/Resources/Images.xcassets/robot.imageset/robot.pdf -------------------------------------------------------------------------------- /Example-iOS/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 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 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIAppFonts 28 | 29 | EBGaramond12-Regular.otf 30 | 31 | UILaunchStoryboardName 32 | Launch Screen 33 | UIMainStoryboardFile 34 | Main 35 | UIRequiredDeviceCapabilities 36 | 37 | armv7 38 | 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Example-iOS/Resources/Launch Screen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Example-iOS/Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example-iOS/StyleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyleViewController.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 8/26/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import UIKit 11 | 12 | /// UITableViewCell's built in labels are re-created when the content size 13 | /// category changes, so we use a cell subclass with a custom label to avoid this. 14 | class BaseTableViewCell: UITableViewCell { 15 | 16 | @IBOutlet var titleLabel: UILabel? 17 | 18 | } 19 | 20 | class StyleViewController: UITableViewController { 21 | var styles: [(String, [NSAttributedString])] = [ 22 | ("Simple Use Case", [DemoStrings.simpleExample]), 23 | ("XML", [ 24 | DemoStrings.xmlExample, 25 | DemoStrings.xmlWithEmphasis, 26 | ]), 27 | ("Composition", [DemoStrings.compositionExample]), 28 | ("Images & Special Characters", [DemoStrings.imagesExample, DemoStrings.noBreakSpaceExample]), 29 | ("Baseline Offset", [DemoStrings.heartsExample]), 30 | ("Indentation", DemoStrings.indentationExamples), 31 | ("Advanced XML and Kerning", [DemoStrings.advancedXMLAndKerningExample]), 32 | ("Dynamic Type", [DemoStrings.dynamicTypeUIKitExample, DemoStrings.preferredFontsExample]), 33 | ("OpenType Features", [ 34 | DemoStrings.figureStylesExample, 35 | DemoStrings.ordinalsExample, 36 | DemoStrings.scientificInferiorsExample, 37 | DemoStrings.fractionsExample, 38 | DemoStrings.stylisticAlternatesExample, 39 | ]), 40 | ("Accessibility Speech", DemoStrings.accessibilitySpeechExamples), 41 | ] 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | tableView.rowHeight = UITableView.automaticDimension 46 | tableView.estimatedRowHeight = 50 47 | } 48 | 49 | func cell(at indexPath: IndexPath) -> UITableViewCell { 50 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "StyleCell", for: indexPath) as? BaseTableViewCell else { 51 | fatalError("Misconfigured VC") 52 | } 53 | let attributedText = styles[indexPath.section].1[indexPath.row] 54 | cell.titleLabel?.attributedText = attributedText.adapted(to: traitCollection) 55 | cell.accessoryType = attributedText.attribute("Storyboard", at: 0, effectiveRange: nil) == nil ? .none : .disclosureIndicator 56 | return cell 57 | } 58 | 59 | override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 60 | let attributedText = styles[indexPath.section].1[indexPath.row] 61 | if attributedText.attribute("Storyboard", at: 0, effectiveRange: nil) is String { 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | func selectRow(at indexPath: IndexPath) { 68 | let attributedText = styles[indexPath.section].1[indexPath.row] 69 | if let storyboardIdentifier = attributedText.attribute("Storyboard", at: 0, effectiveRange: nil) as? String { 70 | guard let nextVC = storyboard?.instantiateViewController(withIdentifier: storyboardIdentifier) else { 71 | fatalError("No Storyboard identifier \(storyboardIdentifier)") 72 | } 73 | navigationController?.pushViewController(nextVC, animated: true) 74 | } 75 | else { 76 | tableView.deselectRow(at: indexPath, animated: true) 77 | } 78 | } 79 | } 80 | 81 | extension StyleViewController { 82 | override func numberOfSections(in tableView: UITableView) -> Int { 83 | return styles.count 84 | } 85 | 86 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 87 | return styles[section].1.count 88 | } 89 | 90 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 91 | return cell(at: indexPath) 92 | } 93 | 94 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 95 | return styles[section].0 96 | } 97 | 98 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 99 | selectRow(at: indexPath) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | gem 'xcpretty' 5 | 6 | # Danger 7 | group :test, :danger do 8 | gem 'slather' 9 | gem 'circleci_artifact' 10 | gem 'xcov' 11 | gem 'fastlane' 12 | end 13 | 14 | group :danger do 15 | gem 'danger' 16 | gem 'danger-swiftlint' 17 | gem 'danger-xcov' 18 | gem 'danger-junit' 19 | end 20 | 21 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 22 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2014 Rightpoint and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "BonMot", 8 | platforms: [ 9 | .iOS(.v11), 10 | .macOS(.v10_11), 11 | .tvOS(.v11), 12 | .watchOS(.v2), 13 | ], 14 | products: [ 15 | .library( 16 | name: "BonMot", 17 | targets: ["BonMot"]), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "BonMot", 22 | dependencies: [], 23 | path: "Sources", 24 | exclude: ["Info.plist"] 25 | ), 26 | .testTarget( 27 | name: "BonMotTests", 28 | dependencies: ["BonMot"], 29 | path: "Tests", 30 | exclude: [ 31 | "Info.plist", 32 | "BonMot-iOSTests.xctestplan", // *.xctestplan didn't seem to work 33 | "BonMot-OSXTests.xctestplan", 34 | "BonMot-tvOSTests.xctestplan", 35 | ], 36 | resources: [ 37 | .process("Resources"), 38 | ]), 39 | ], 40 | swiftLanguageVersions: [.v5] 41 | ) 42 | -------------------------------------------------------------------------------- /Resources/Licenses.txt: -------------------------------------------------------------------------------- 1 | License for EB Garamond Font: 2 | 3 | Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at) 4 | 5 | All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | 10 | ----------------------------------------------------------- 11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 12 | ----------------------------------------------------------- 13 | 14 | PREAMBLE 15 | The goals of the Open Font License (OFL) are to stimulate worldwide 16 | development of collaborative font projects, to support the font creation 17 | efforts of academic and linguistic communities, and to provide a free and 18 | open framework in which fonts may be shared and improved in partnership 19 | with others. 20 | 21 | The OFL allows the licensed fonts to be used, studied, modified and 22 | redistributed freely as long as they are not sold by themselves. The 23 | fonts, including any derivative works, can be bundled, embedded, 24 | redistributed and/or sold with any software provided that any reserved 25 | names are not used by derivative works. The fonts and derivatives, 26 | however, cannot be released under any other type of license. The 27 | requirement for fonts to remain under this license does not apply 28 | to any document created using the fonts or their derivatives. 29 | 30 | DEFINITIONS 31 | "Font Software" refers to the set of files released by the Copyright 32 | Holder(s) under this license and clearly marked as such. This may 33 | include source files, build scripts and documentation. 34 | 35 | "Reserved Font Name" refers to any names specified as such after the 36 | copyright statement(s). 37 | 38 | "Original Version" refers to the collection of Font Software components as 39 | distributed by the Copyright Holder(s). 40 | 41 | "Modified Version" refers to any derivative made by adding to, deleting, 42 | or substituting -- in part or in whole -- any of the components of the 43 | Original Version, by changing formats or by porting the Font Software to a 44 | new environment. 45 | 46 | "Author" refers to any designer, engineer, programmer, technical 47 | writer or other person who contributed to the Font Software. 48 | 49 | PERMISSION & CONDITIONS 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 52 | redistribute, and sell modified and unmodified copies of the Font 53 | Software, subject to the following conditions: 54 | 55 | 1) Neither the Font Software nor any of its individual components, 56 | in Original or Modified Versions, may be sold by itself. 57 | 58 | 2) Original or Modified Versions of the Font Software may be bundled, 59 | redistributed and/or sold with any software, provided that each copy 60 | contains the above copyright notice and this license. These can be 61 | included either as stand-alone text files, human-readable headers or 62 | in the appropriate machine-readable metadata fields within text or 63 | binary files as long as those fields can be easily viewed by the user. 64 | 65 | 3) No Modified Version of the Font Software may use the Reserved Font 66 | Name(s) unless explicit written permission is granted by the corresponding 67 | Copyright Holder. This restriction only applies to the primary font name as 68 | presented to the users. 69 | 70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 71 | Software shall not be used to promote, endorse or advertise any 72 | Modified Version, except to acknowledge the contribution(s) of the 73 | Copyright Holder(s) and the Author(s) or with their explicit written 74 | permission. 75 | 76 | 5) The Font Software, modified or unmodified, in part or in whole, 77 | must be distributed entirely under this license, and must not be 78 | distributed under any other license. The requirement for fonts to 79 | remain under this license does not apply to any document created 80 | using the Font Software. 81 | 82 | TERMINATION 83 | This license becomes null and void if any of the above conditions are 84 | not met. 85 | 86 | DISCLAIMER 87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 95 | OTHER DEALINGS IN THE FONT SOFTWARE. 96 | 97 | 98 | License for Tennis Racket Icon: 99 | Created by Gabriele Fumero from the Noun Project 100 | -------------------------------------------------------------------------------- /Resources/assets.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/assets.sketch -------------------------------------------------------------------------------- /Resources/readme-images/BonMot-logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/BonMot-logo.ai -------------------------------------------------------------------------------- /Resources/readme-images/BonMot-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/BonMot-logo.png -------------------------------------------------------------------------------- /Resources/readme-images/bon-mot-style-attributes-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/bon-mot-style-attributes-inspector.png -------------------------------------------------------------------------------- /Resources/readme-images/fish-with-black-comma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/fish-with-black-comma.png -------------------------------------------------------------------------------- /Resources/readme-images/ios-type-scaling-behavior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/ios-type-scaling-behavior.png -------------------------------------------------------------------------------- /Resources/readme-images/label-with-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/label-with-icon.png -------------------------------------------------------------------------------- /Resources/readme-images/text-alignment-attributes-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/text-alignment-attributes-inspector.png -------------------------------------------------------------------------------- /Resources/readme-images/text-alignment-identity-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/text-alignment-identity-inspector.png -------------------------------------------------------------------------------- /Resources/readme-images/text-alignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/text-alignment.png -------------------------------------------------------------------------------- /Resources/readme-images/wrapped-label-with-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Resources/readme-images/wrapped-label-with-icon.png -------------------------------------------------------------------------------- /Sources/AccessibilityHeadingLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityHeadingLevel.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 9/21/17. 6 | // Copyright © 2017 Rightpoint. All rights reserved. 7 | // 8 | 9 | public enum HeadingLevel: Int { 10 | 11 | case none = 0 12 | case one = 1 13 | case two = 2 14 | case three = 3 15 | case four = 4 16 | case five = 5 17 | case six = 6 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/BonMot.h: -------------------------------------------------------------------------------- 1 | // 2 | // BonMot.h 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/24/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for BonMot. 12 | FOUNDATION_EXPORT double BonMotVersionNumber; 13 | 14 | //! Project version string for BonMot. 15 | FOUNDATION_EXPORT const unsigned char BonMotVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Compatibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compatibility.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 8/24/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | // This file declares extensions to system types to provide a compatible API 16 | // between Swift iOS, macOS, watchOS, and tvOS. 17 | 18 | #if os(OSX) 19 | #else 20 | public extension NSParagraphStyle { 21 | 22 | typealias LineBreakMode = NSLineBreakMode 23 | 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/ContextualAlternates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextualAlternates.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 11/5/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | // This is not supported on watchOS 16 | #if os(iOS) || os(tvOS) || os(OSX) 17 | 18 | /// Different contextual alternates available for customizing a font. Not 19 | /// all fonts support all (or any) of these options. 20 | public struct ContextualAlternates { 21 | 22 | var contextualAlternates: Bool? 23 | var swashAlternates: Bool? 24 | var contextualSwashAlternates: Bool? 25 | 26 | public init() { } 27 | 28 | } 29 | 30 | // Convenience constructors 31 | extension ContextualAlternates { 32 | 33 | public static func contextualAlternates(on isOn: Bool) -> ContextualAlternates { 34 | var alts = ContextualAlternates() 35 | alts.contextualAlternates = isOn 36 | return alts 37 | } 38 | 39 | public static func swashAlternates(on isOn: Bool) -> ContextualAlternates { 40 | var alts = ContextualAlternates() 41 | alts.swashAlternates = isOn 42 | return alts 43 | } 44 | 45 | public static func contextualSwashAlternates(on isOn: Bool) -> ContextualAlternates { 46 | var alts = ContextualAlternates() 47 | alts.contextualSwashAlternates = isOn 48 | return alts 49 | } 50 | 51 | } 52 | 53 | extension ContextualAlternates { 54 | 55 | mutating public func add(other theOther: ContextualAlternates) { 56 | contextualAlternates = theOther.contextualAlternates ?? contextualAlternates 57 | swashAlternates = theOther.swashAlternates ?? swashAlternates 58 | contextualSwashAlternates = theOther.contextualSwashAlternates ?? contextualSwashAlternates 59 | } 60 | 61 | public func byAdding(other theOther: ContextualAlternates) -> ContextualAlternates { 62 | var varSelf = self 63 | varSelf.add(other: theOther) 64 | return varSelf 65 | } 66 | 67 | } 68 | 69 | extension ContextualAlternates: FontFeatureProvider { 70 | 71 | public func featureSettings() -> [(type: Int, selector: Int)] { 72 | var selectors = [Int]() 73 | 74 | if let contextualAlternates = contextualAlternates { 75 | selectors.append(contextualAlternates ? kContextualAlternatesOnSelector : kContextualAlternatesOffSelector) 76 | } 77 | if let swashAlternates = swashAlternates { 78 | selectors.append(swashAlternates ? kSwashAlternatesOnSelector : kSwashAlternatesOffSelector) 79 | } 80 | if let contextualSwashAlternates = contextualSwashAlternates { 81 | selectors.append(contextualSwashAlternates ? kContextualSwashAlternatesOnSelector : kContextualSwashAlternatesOffSelector) 82 | } 83 | 84 | return selectors.map { (type: kContextualAlternatesType, selector: $0) } 85 | } 86 | 87 | } 88 | 89 | extension ContextualAlternates { 90 | public static func + (lhs: ContextualAlternates, rhs: ContextualAlternates) -> ContextualAlternates { 91 | return lhs.byAdding(other: rhs) 92 | } 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /Sources/Emphasis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emphasis.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 2/12/18. 6 | // Copyright © 2018 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | public struct Emphasis: OptionSet { 16 | 17 | public var rawValue: Int 18 | 19 | public init(rawValue: Int) { 20 | self.rawValue = rawValue 21 | } 22 | 23 | public static let italic = Emphasis(rawValue: 1 << 0) 24 | public static let bold = Emphasis(rawValue: 1 << 1) 25 | 26 | // Reserved for later use, if we figure out a good naming scheme and use case. 27 | private static let expanded = Emphasis(rawValue: 1 << 2) 28 | private static let condensed = Emphasis(rawValue: 1 << 3) 29 | private static let vertical = Emphasis(rawValue: 1 << 4) 30 | private static let uiOptimized = Emphasis(rawValue: 1 << 5) 31 | private static let tightLineSpacing = Emphasis(rawValue: 1 << 6) // AKA Tight Leading 32 | private static let looseLineSpacing = Emphasis(rawValue: 1 << 7) // AKA Loose Leading 33 | 34 | } 35 | 36 | extension Emphasis { 37 | 38 | var symbolicTraits: BONSymbolicTraits { 39 | var traits: BONSymbolicTraits = [] 40 | if contains(.italic) { 41 | traits.insert(.italic) 42 | } 43 | if contains(.bold) { 44 | traits.insert(.bold) 45 | } 46 | if contains(.expanded) { 47 | traits.insert(.expanded) 48 | } 49 | if contains(.condensed) { 50 | traits.insert(.condensed) 51 | } 52 | if contains(.vertical) { 53 | traits.insert(.vertical) 54 | } 55 | if contains(.uiOptimized) { 56 | traits.insert(.uiOptimized) 57 | } 58 | if contains(.tightLineSpacing) { 59 | traits.insert(.tightLineSpacing) 60 | } 61 | if contains(.looseLineSpacing) { 62 | traits.insert(.looseLineSpacing) 63 | } 64 | 65 | return traits 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Image+Tinting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+Tinting.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if os(OSX) 12 | import AppKit 13 | #else 14 | import UIKit 15 | #endif 16 | 17 | public extension BONImage { 18 | 19 | #if os(OSX) 20 | /// Returns a copy of the receiver where the alpha channel is maintained, 21 | /// but every pixel's color is replaced with `color`. 22 | /// 23 | /// - note: The returned image does _not_ have the template flag set, 24 | /// preventing further tinting. 25 | /// 26 | /// - Parameter theColor: The color to use to tint the receiver. 27 | /// - Returns: A tinted copy of the image. 28 | @objc(bon_tintedImageWithColor:) 29 | func tintedImage(color theColor: BONColor) -> BONImage { 30 | let imageRect = CGRect(origin: .zero, size: size) 31 | 32 | let image = NSImage(size: size) 33 | 34 | let rep = NSBitmapImageRep( 35 | bitmapDataPlanes: nil, 36 | pixelsWide: Int(size.width), 37 | pixelsHigh: Int(size.height), 38 | bitsPerSample: 8, 39 | samplesPerPixel: 4, 40 | hasAlpha: true, 41 | isPlanar: false, 42 | colorSpaceName: theColor.colorSpaceName, 43 | bytesPerRow: 0, 44 | bitsPerPixel: 0 45 | )! 46 | 47 | image.addRepresentation(rep) 48 | 49 | image.lockFocus() 50 | 51 | let context = NSGraphicsContext.current!.cgContext 52 | 53 | context.setBlendMode(.normal) 54 | let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil)! 55 | context.draw(cgImage, in: imageRect) 56 | 57 | // .sourceIn: resulting color = source color * destination alpha 58 | context.setBlendMode(.sourceIn) 59 | context.setFillColor(theColor.cgColor) 60 | context.fill(imageRect) 61 | 62 | image.unlockFocus() 63 | 64 | // Prevent further tinting 65 | image.isTemplate = false 66 | 67 | // Transfer accessibility description 68 | image.accessibilityDescription = self.accessibilityDescription 69 | 70 | return image 71 | } 72 | #else 73 | /// Returns a copy of the receiver where the alpha channel is maintained, 74 | /// but every pixel's color is replaced with `color`. 75 | /// 76 | /// - note: The returned image does _not_ have the template flag set, 77 | /// preventing further tinting. 78 | /// 79 | /// - Parameter theColor: The color to use to tint the receiver. 80 | /// - Returns: A tinted copy of the image. 81 | @objc(bon_tintedImageWithColor:) 82 | func tintedImage(color theColor: BONColor) -> BONImage { 83 | let imageRect = CGRect(origin: .zero, size: size) 84 | // Save original properties 85 | let originalCapInsets = capInsets 86 | let originalResizingMode = resizingMode 87 | let originalAlignmentRectInsets = alignmentRectInsets 88 | 89 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 90 | let context = UIGraphicsGetCurrentContext()! 91 | 92 | // Flip the context vertically 93 | context.translateBy(x: 0.0, y: size.height) 94 | context.scaleBy(x: 1.0, y: -1.0) 95 | 96 | // Image tinting mostly inspired by http://stackoverflow.com/a/22528426/255489 97 | 98 | context.setBlendMode(.normal) 99 | context.draw(cgImage!, in: imageRect) 100 | 101 | // .sourceIn: resulting color = source color * destination alpha 102 | context.setBlendMode(.sourceIn) 103 | context.setFillColor(theColor.cgColor) 104 | context.fill(imageRect) 105 | 106 | // Get new image 107 | var image = UIGraphicsGetImageFromCurrentImageContext()! 108 | UIGraphicsEndImageContext() 109 | 110 | // Prevent further tinting 111 | image = image.withRenderingMode(.alwaysOriginal) 112 | 113 | // Restore original properties 114 | image = image.withAlignmentRectInsets(originalAlignmentRectInsets) 115 | if originalCapInsets != image.capInsets || originalResizingMode != image.resizingMode { 116 | image = image.resizableImage(withCapInsets: originalCapInsets, resizingMode: originalResizingMode) 117 | } 118 | 119 | // Transfer accessibility label (watchOS not included; does not have accessibilityLabel on UIImage). 120 | #if os(iOS) || os(tvOS) 121 | image.accessibilityLabel = self.accessibilityLabel 122 | #endif 123 | 124 | return image 125 | } 126 | #endif 127 | 128 | } 129 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Ligatures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ligatures.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 11/1/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | /// Different ligature styles for use in attributed strings. 10 | public enum Ligatures: Int { 11 | 12 | /// No ligatures. 13 | case disabled = 0 14 | 15 | /// Default ligatures. 16 | case defaults = 1 17 | 18 | #if os(OSX) 19 | /// All ligatures. 20 | case all = 2 21 | #endif 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/MutableCopying.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableCopying.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if os(iOS) || os(watchOS) || os(tvOS) 12 | import UIKit 13 | #else 14 | import AppKit 15 | #endif 16 | 17 | extension NSAttributedString { 18 | 19 | @nonobjc func mutableStringCopy() -> NSMutableAttributedString { 20 | guard let copy = mutableCopy() as? NSMutableAttributedString else { 21 | fatalError("Failed to mutableCopy() \(self)") 22 | } 23 | return copy 24 | } 25 | 26 | @nonobjc func immutableCopy() -> NSAttributedString { 27 | guard let copy = copy() as? NSAttributedString else { 28 | fatalError("Failed to copy() \(self)") 29 | } 30 | return copy 31 | } 32 | } 33 | 34 | extension NSParagraphStyle { 35 | 36 | @nonobjc func mutableParagraphStyleCopy() -> NSMutableParagraphStyle { 37 | guard let copy = mutableCopy() as? NSMutableParagraphStyle else { 38 | fatalError("Failed to mutableCopy() \(self)") 39 | } 40 | return copy 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/NSAttributedString+BonMot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+BonMot.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 7/19/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | extension NSAttributedString { 16 | 17 | /// Create a copy of `self`, but replace characters in the `Special` 18 | /// enumeration, images, and unassigned unicode characters with a 19 | /// human-readable string. 20 | public var bonMotDebugAttributedString: NSAttributedString { 21 | let debug = mutableStringCopy() 22 | var replacements = [(range: NSRange, string: String)]() 23 | var index = 0 24 | 25 | // When looping over `string.unicodeScalars` directly, we saw 26 | // nondeterministic behavior where indices after the first one would 27 | // contain different characters than what was expected. Pulling 28 | /// `unicodeScalars` out first, and then looping, seems to fix it. 29 | let scalars = string.unicodeScalars 30 | 31 | for unicode in scalars { 32 | let replacementString: String? 33 | switch Special(rawValue: String(unicode)) { 34 | case .space?: 35 | replacementString = nil 36 | case .objectReplacementCharacter?: 37 | #if os(iOS) || os(tvOS) || os(OSX) 38 | if let attachment = attribute(.attachment, at: index, effectiveRange: nil) as? NSTextAttachment, let image = attachment.image { 39 | replacementString = String(format: "image size='%.3gx%.3g'", image.size.width, image.size.height) 40 | } 41 | else { 42 | replacementString = Special.objectReplacementCharacter.name 43 | } 44 | #else 45 | replacementString = nil 46 | #endif 47 | case let value: 48 | replacementString = value?.name 49 | } 50 | let utf16Length = String(unicode).utf16.count 51 | if let replacementString = replacementString { 52 | replacements.append((NSRange(location: index, length: utf16Length), replacementString)) 53 | } 54 | index += utf16Length 55 | } 56 | for replacement in replacements.reversed() { 57 | debug.replaceCharacters(in: replacement.range, with: "") 58 | } 59 | replacements = [] 60 | 61 | let unassignedPrefix = "\\N{ Void = { name in 39 | print("Requesting unregistered style \(name)") 40 | } 41 | 42 | /// Create a new `NamedStyles` object with the specified name-to-style 43 | /// mapping. 44 | /// - parameter styles: A dictionary containing the name-to-style mapping 45 | public init(styles: [String: StringStyle] = [:]) { 46 | self.styles = styles 47 | } 48 | 49 | /// The name-to-style mapping 50 | public var styles: [String: StringStyle] 51 | 52 | /// Register a new named style for later retrieval. 53 | /// 54 | /// - Parameters: 55 | /// - name: The name of the new style. If a style is already registered 56 | /// for this name, it is replaced. 57 | /// - style: The style to register. 58 | public func registerStyle(forName name: String, style: StringStyle) { 59 | styles[name] = style 60 | } 61 | 62 | /// Look up a style for the specified name. If no style is found, 63 | /// `NamedStyles.unregisteredStyleClosure` is called. This is done for error 64 | /// reporting and safety. We don't want to crash if no style is found, and 65 | /// we want to avoid adding `throws` everywhere. In general, if a style is 66 | /// requested by name, it will just log, and your text will be un-styled. 67 | /// 68 | /// - parameter forName: The name of the style to look up 69 | /// - returns: the requested style, or `nil` if none is found 70 | public func style(forName name: String) -> StringStyle? { 71 | guard let style = styles[name] else { 72 | NamedStyles.unregisteredStyleClosure(name) 73 | return nil 74 | } 75 | return style 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Platform.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/22/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | public typealias BONColor = NSColor 12 | public typealias BONImage = NSImage 13 | public typealias BONTextField = NSTextField 14 | 15 | public typealias BONFont = NSFont 16 | public typealias BONFontDescriptor = NSFontDescriptor 17 | public typealias BONSymbolicTraits = NSFontDescriptor.SymbolicTraits 18 | let BONFontDescriptorFeatureSettingsAttribute = NSFontDescriptor.AttributeName.featureSettings 19 | let BONFontFeatureTypeIdentifierKey = NSFontDescriptor.FeatureKey.typeIdentifier 20 | let BONFontFeatureSelectorIdentifierKey = NSFontDescriptor.FeatureKey.selectorIdentifier 21 | #else 22 | import UIKit 23 | public typealias BONColor = UIColor 24 | public typealias BONImage = UIImage 25 | 26 | public typealias BONFont = UIFont 27 | public typealias BONFontDescriptor = UIFontDescriptor 28 | public typealias BONSymbolicTraits = UIFontDescriptor.SymbolicTraits 29 | let BONFontDescriptorFeatureSettingsAttribute = UIFontDescriptor.AttributeName.featureSettings 30 | let BONFontFeatureTypeIdentifierKey = UIFontDescriptor.FeatureKey.featureIdentifier 31 | let BONFontFeatureSelectorIdentifierKey = UIFontDescriptor.FeatureKey.typeIdentifier 32 | 33 | #if os(iOS) || os(tvOS) 34 | public typealias BONTextField = UITextField 35 | #endif 36 | #endif 37 | 38 | public typealias StyleAttributes = [NSAttributedString.Key: Any] 39 | 40 | #if os(iOS) || os(tvOS) 41 | public typealias BonMotTextStyle = UIFont.TextStyle 42 | public typealias BonMotContentSizeCategory = UIContentSizeCategory 43 | #endif 44 | 45 | // This key is defined here because it needs to be used in non-adaptive code. 46 | public let BonMotTransformationsAttributeName = NSAttributedString.Key("BonMotTransformations") 47 | 48 | extension BONSymbolicTraits { 49 | #if os(iOS) || os(tvOS) || os(watchOS) 50 | static var italic: BONSymbolicTraits { 51 | return .traitItalic 52 | } 53 | static var bold: BONSymbolicTraits { 54 | return .traitBold 55 | } 56 | static var expanded: BONSymbolicTraits { 57 | return .traitExpanded 58 | } 59 | static var condensed: BONSymbolicTraits { 60 | return .traitCondensed 61 | } 62 | static var vertical: BONSymbolicTraits { 63 | return .traitVertical 64 | } 65 | static var uiOptimized: BONSymbolicTraits { 66 | return .traitUIOptimized 67 | } 68 | static var tightLineSpacing: BONSymbolicTraits { 69 | return .traitTightLeading 70 | } 71 | static var looseLineSpacing: BONSymbolicTraits { 72 | return .traitLooseLeading 73 | } 74 | #else 75 | static var uiOptimized: BONSymbolicTraits { 76 | return .UIOptimized 77 | } 78 | static var tightLineSpacing: BONSymbolicTraits { 79 | return .tightLeading 80 | } 81 | static var looseLineSpacing: BONSymbolicTraits { 82 | return .looseLeading 83 | } 84 | #endif 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Special.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Special.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/1/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | /// Interesting Unicode characters for use in creating strings. Most characters 10 | /// in `Special` are either non-printing (like the various space characters) or 11 | /// visually ambiguous when viewed with a monospace code font (like the dashes 12 | /// and hyphens). 13 | public enum Special: String, CaseIterable { 14 | 15 | // Keep the cases sorted by unichar value when adding new cases. 16 | case tab = "\u{0009}" 17 | case lineFeed = "\u{000A}" 18 | case verticalTab = "\u{000B}" 19 | case formFeed = "\u{000C}" 20 | case carriageReturn = "\u{000D}" 21 | case space = "\u{0020}" 22 | case nextLine = "\u{0085}" 23 | case noBreakSpace = "\u{00A0}" 24 | case enSpace = "\u{2002}" 25 | case emSpace = "\u{2003}" 26 | case figureSpace = "\u{2007}" 27 | case thinSpace = "\u{2009}" 28 | case hairSpace = "\u{200A}" 29 | case zeroWidthSpace = "\u{200B}" 30 | case nonBreakingHyphen = "\u{2011}" 31 | case figureDash = "\u{2012}" 32 | case enDash = "\u{2013}" 33 | case emDash = "\u{2014}" 34 | case horizontalEllipsis = "\u{2026}" 35 | case lineSeparator = "\u{2028}" 36 | case paragraphSeparator = "\u{2029}" 37 | case leftToRightOverride = "\u{202D}" 38 | case narrowNoBreakSpace = "\u{202F}" 39 | case wordJoiner = "\u{2060}" 40 | case minusSign = "\u{2212}" 41 | case objectReplacementCharacter = "\u{FFFC}" // NSAttachmentCharacter 42 | 43 | } 44 | 45 | extension Special: CustomStringConvertible { 46 | 47 | /// A `String` initialized the `UnicodeScalar` of the receiver as its `rawValue`. 48 | public var description: String { 49 | return String(rawValue) 50 | } 51 | 52 | } 53 | 54 | extension Special { 55 | 56 | /// A developer-facing string for this UnicodeValue. Useful for debugging. 57 | public var name: String { 58 | switch self { 59 | case .tab: return "tab" 60 | case .lineFeed: return "lineFeed" 61 | case .verticalTab: return "verticalTab" 62 | case .formFeed: return "formFeed" 63 | case .carriageReturn: return "carriageReturn" 64 | case .space: return "space" 65 | case .nextLine: return "nextLine" 66 | case .noBreakSpace: return "noBreakSpace" 67 | case .enSpace: return "enSpace" 68 | case .emSpace: return "emSpace" 69 | case .figureSpace: return "figureSpace" 70 | case .thinSpace: return "thinSpace" 71 | case .hairSpace: return "hairSpace" 72 | case .zeroWidthSpace: return "zeroWidthSpace" 73 | case .nonBreakingHyphen: return "nonBreakingHyphen" 74 | case .figureDash: return "figureDash" 75 | case .enDash: return "enDash" 76 | case .emDash: return "emDash" 77 | case .horizontalEllipsis: return "horizontalEllipsis" 78 | case .lineSeparator: return "lineSeparator" 79 | case .paragraphSeparator: return "paragraphSeparator" 80 | case .leftToRightOverride: return "leftToRightOverride" 81 | case .narrowNoBreakSpace: return "narrowNoBreakSpace" 82 | case .wordJoiner: return "wordJoiner" 83 | case .minusSign: return "minusSign" 84 | case .objectReplacementCharacter: return "objectReplacementCharacter" 85 | } 86 | } 87 | 88 | /// All of the enum values contained in `Special`. 89 | /// Property kept here for backward compatibility 90 | @available(*, deprecated, renamed: "allCases") 91 | @inlinable 92 | public static var all: [Special] { allCases } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Tab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tab.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | /// Creates a tab (\t) character with a calculated space from the beginning of the line. 16 | public enum Tab { 17 | 18 | /// A spacer `Tab` introduces a tab of the specified amount from the current 19 | /// position in the `String`, much like tab stops in a word processor. 20 | case spacer(CGFloat) 21 | 22 | /// A head indent `Tab` will introduce a tab of the specified amount from 23 | /// the current position in the string, and update the `headIndent` value in 24 | /// the containing `NSParagraphStyle`. 25 | case headIndent(CGFloat) 26 | 27 | } 28 | 29 | extension Tab: Composable { 30 | 31 | public func append(to attributedString: NSMutableAttributedString, baseStyle: StringStyle, isLastElement: Bool) { 32 | let attributes = baseStyle.attributes 33 | #if os(iOS) 34 | // Embed the tab in the attributes 35 | let tabAttributes = EmbeddedTransformationHelpers.embed(transformation: self, to: attributes) 36 | #else 37 | let tabAttributes: StyleAttributes = attributes 38 | #endif 39 | let tabRange = NSRange(location: attributedString.length, length: 1) 40 | 41 | attributedString.append(NSAttributedString(string: Special.tab.description, attributes: tabAttributes)) 42 | 43 | // Calculate the tab spacing 44 | update(string: attributedString, in: tabRange) 45 | } 46 | 47 | } 48 | 49 | extension Tab { 50 | 51 | /// Update the tab calculation for the tabs in `range`. This will create an 52 | /// `NSTabStop` in the paragraph style with the specified padding from the 53 | /// beginning of the line. This supports multiple tabs in one line and 54 | /// multiple lines. 55 | /// 56 | /// This implementation conforms to `AttributedStringTransformation`, but 57 | /// since this is used when the adaptive code may not be included, the 58 | /// conformance is not declared here. It is declared in Tab+Adaptive.swift. 59 | /// 60 | /// - Parameters: 61 | /// - attributedString: The attributed string to update. 62 | /// - range: The range on which to perform the tab calculations. 63 | func update(string attributedString: NSMutableAttributedString, in range: NSRange) { 64 | let string = attributedString.string as NSString 65 | 66 | // Lookup the range this paragraph is operating on. 67 | // This is the range from `range` to the preceding newline or the start of the string. 68 | let precedingRange = NSRange(location: 0, length: NSMaxRange(range)) 69 | var leadingNewline = string.rangeOfCharacter(from: CharacterSet.newlines, options: [.backwards], range: precedingRange).location 70 | leadingNewline = (leadingNewline == NSNotFound) ? 0 : leadingNewline + 1 71 | let paragraphRange = NSRange(location: leadingNewline, length: NSMaxRange(range) - leadingNewline) 72 | 73 | // Search backwards by attribute cluster to obtain the paragraph inside of `paragraphRange`. 74 | var paragraphCursor = range.location 75 | var paragraphAttribute: Any? 76 | while paragraphCursor >= leadingNewline && paragraphAttribute == nil { 77 | var attributeRange = NSRange() 78 | let attributes = attributedString.attributes(at: paragraphCursor, effectiveRange: &attributeRange) 79 | paragraphAttribute = attributes[.paragraphStyle] 80 | paragraphCursor = attributeRange.location - 1 81 | } 82 | 83 | // Prepare the NSMutableParagraphStyle and configure it over the paragraphRange 84 | let paragraph: NSMutableParagraphStyle 85 | if paragraphAttribute == nil { 86 | paragraph = NSMutableParagraphStyle() 87 | } 88 | else if let existingParagraph = paragraphAttribute as? NSMutableParagraphStyle { 89 | paragraph = existingParagraph 90 | } 91 | else if let existingParagraph = paragraphAttribute as? NSParagraphStyle { 92 | paragraph = existingParagraph.mutableParagraphStyleCopy() 93 | } 94 | else { 95 | fatalError("Non paragraphStyle held in NSParagraphStyleAttributeName.") 96 | } 97 | 98 | // Enumerate tabs over the range of the paragraph, keeping count of the tabs. 99 | var enumerationRange = paragraphRange 100 | var tabIndex = 0 101 | while true { 102 | let tabRange = string.range(of: "\t", options: [], range: enumerationRange) 103 | guard tabRange.location != NSNotFound else { break } 104 | 105 | // If the tab is in `range`, recalculate the tab at tabIndex. 106 | if NSLocationInRange(tabRange.location, range) { 107 | 108 | // Calculate the length of the string before the tab. Since tabs are relative to the paragraph range, 109 | // start at the start of the effective range for NSParagraphStyleAttributeName 110 | let preTab = attributedString.attributedSubstring(from: NSRange(location: paragraphRange.location, length: tabRange.location - paragraphRange.location)) 111 | let max = CGSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude) 112 | let contentWidth = preTab.boundingRect(with: max, options: .usesLineFragmentOrigin, context: nil).width 113 | 114 | // Add the padding and update the NSTextTab at tabIndex. 115 | let tabStop = contentWidth + padding 116 | paragraph.tabStops[tabIndex] = NSTextTab(textAlignment: .natural, location: tabStop, options: [:]) 117 | 118 | // Update the paragraph object if it is a headIndent tab. 119 | if case .headIndent = self { 120 | paragraph.headIndent = tabStop 121 | } 122 | } 123 | // Update the enumerationRange and tabIndex for the next pass 124 | enumerationRange.length = NSMaxRange(enumerationRange) - NSMaxRange(tabRange) 125 | enumerationRange.location = NSMaxRange(tabRange) 126 | tabIndex += 1 127 | } 128 | attributedString.addAttribute(.paragraphStyle, value: paragraph, range: paragraphRange) 129 | } 130 | 131 | var padding: CGFloat { 132 | switch self { 133 | case let .spacer(padding): return padding 134 | case let .headIndent(padding): return padding 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /Sources/Tracking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracking.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/26/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | /// An enumeration representing the tracking to be applied. 12 | public enum Tracking { 13 | 14 | case point(CGFloat) 15 | case adobe(CGFloat) 16 | 17 | public func kerning(for font: BONFont?) -> CGFloat { 18 | switch self { 19 | case .point(let kernValue): 20 | return kernValue 21 | case .adobe(let adobeTracking): 22 | let AdobeTrackingDivisor: CGFloat = 1000.0 23 | if font == nil { 24 | print("Can not apply tracking to style when no font is defined, using 0 instead") 25 | } 26 | let pointSize = font?.pointSize ?? 0 27 | return pointSize * (adobeTracking / AdobeTrackingDivisor) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Transform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transform.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 3/24/17. 6 | // Copyright © 2017 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | public enum Transform { 16 | 17 | public typealias TransformFunction = (String) -> String 18 | 19 | case lowercase 20 | case uppercase 21 | case capitalized 22 | 23 | case lowercaseWithLocale(Locale) 24 | case uppercaseWithLocale(Locale) 25 | case capitalizedWithLocale(Locale) 26 | case custom(TransformFunction) 27 | 28 | var transformer: TransformFunction { 29 | switch self { 30 | case .lowercase: return { string in string.localizedLowercase } 31 | case .uppercase: return { string in string.localizedUppercase } 32 | case .capitalized: return { string in string.localizedCapitalized } 33 | 34 | case .lowercaseWithLocale(let locale): return { string in string.lowercased(with: locale) } 35 | case .uppercaseWithLocale(let locale): return { string in string.uppercased(with: locale) } 36 | case .capitalizedWithLocale(let locale): return { string in string.capitalized(with: locale) } 37 | 38 | case .custom(let transform): return transform 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/UIKit/AdaptiveStyleTransformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptiveStyleTransformation.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/20/16. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | /// Defines a style transformation that is dependent on a `UITraitCollection`. 13 | /// An adaptive transformation is embedded in the `StyleAttributes` so that any 14 | /// `NSAttributedString` can be updated to a new trait collection using 15 | /// `attributedString.adapted(to: traitCollection)`. 16 | /// 17 | /// Since `NSAttributedString` conforms to `NSCoding`, `AdaptiveStyleTransformation` 18 | /// is embedded in the `StyleAttributes` via simple dictionary encoding. 19 | /// `NSCoding` was avoided so that value types can conform. See 20 | /// `EmbeddedTransformation` for more information. 21 | internal protocol AdaptiveStyleTransformation { 22 | 23 | /// Change any of `theAttributes`, as desired, to the specified 24 | /// `traitCollection` and return a new `StyleAttributes` dictionary. 25 | /// 26 | /// - Parameters: 27 | /// - theAttributes: The input attributes. 28 | /// - traitCollection: The trait collection to adapt to. 29 | /// - Returns: The adapted attributes, if any. 30 | func adapt(attributes theAttributes: StyleAttributes, to traitCollection: UITraitCollection) -> StyleAttributes? 31 | 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/UIKit/AttributedStringTransformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedStringTransformation.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Defines a transformation to be performed on an `NSMutableAttributedString`. 12 | /// It is used for adaptive transformations that need to know about the content 13 | /// of the string in order to be performed. These are applied after the 14 | /// `AdaptiveStyleTransformation`s are applied. 15 | internal protocol AttributedStringTransformation { 16 | 17 | /// Recalculate any values in the string over the specified range. 18 | /// 19 | /// - parameter string: The attributed string to be updated. 20 | /// - parameter in: The range to operate over. 21 | func update(string theString: NSMutableAttributedString, in range: NSRange) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/UIKit/EmbeddedTransformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbeddedTransformation.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | /// BonMot embeds transformation objects inside `NSAttributedString` attributes 13 | /// to do adaptive styling. To simplify `NSAttributedString`'s `NSCoding` 14 | /// support, these transformations get embedded using plist-compatible objects. 15 | /// This protocol defines a contract to simplify this. `NSCoding` is not used so 16 | /// that value types can conform. 17 | internal protocol EmbeddedTransformation { 18 | 19 | /// Return a plist-compatible dictionary of any state that is needed to 20 | /// persist the adaptation 21 | var asDictionary: StyleAttributes { get } 22 | 23 | /// Take the adaptations dictionary and create an array of 24 | /// `AdaptiveStyleTransformation`s. To register a new adaptive transformation, 25 | /// add the type to `EmbeddedTransformationHelpers.embeddedTransformationTypes`. 26 | static func from(dictionary dict: StyleAttributes) -> EmbeddedTransformation? 27 | 28 | } 29 | 30 | // Helpers for managing keys in the `StyleAttributes` related to adaptive functionality. 31 | internal enum EmbeddedTransformationHelpers { 32 | 33 | struct Key { 34 | 35 | static let type = NSAttributedString.Key("type") 36 | static let size = NSAttributedString.Key("size") 37 | static let textStyle = NSAttributedString.Key("textStyle") 38 | static let maxPointSize = NSAttributedString.Key("maxPointSize") 39 | 40 | } 41 | 42 | static var embeddedTransformationTypes: [EmbeddedTransformation.Type] = [AdaptiveStyle.self, Tracking.self, Tab.self] 43 | 44 | static func embed(transformation theTransformation: EmbeddedTransformation, to styleAttributes: StyleAttributes) -> StyleAttributes { 45 | let dictionary = theTransformation.asDictionary 46 | var styleAttributes = styleAttributes 47 | var adaptations = styleAttributes[BonMotTransformationsAttributeName] as? [StyleAttributes] ?? [] 48 | 49 | // Only add the transformation once. 50 | let contains = adaptations.contains { NSDictionary(dictionary: $0) == NSDictionary(dictionary: dictionary) } 51 | if !contains { 52 | adaptations.append(dictionary) 53 | } 54 | styleAttributes[BonMotTransformationsAttributeName] = adaptations 55 | return styleAttributes 56 | } 57 | 58 | static func transformations(from styleAttributes: StyleAttributes) -> [T] { 59 | let representations = styleAttributes[BonMotTransformationsAttributeName] as? [StyleAttributes] ?? [] 60 | let results: [T?] = representations.map { representation in 61 | for type in embeddedTransformationTypes { 62 | if let transformation = type.from(dictionary: representation) as? T { 63 | return transformation 64 | } 65 | } 66 | return nil 67 | } 68 | return results.compactMap({ $0 }) 69 | } 70 | 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/UIKit/NSAttributedString+Adaptive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Adapt.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/20/16. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | extension NSAttributedString { 13 | 14 | /// Adapt a set of attributes to the specified trait collection. This will 15 | /// use the style object defined in the attributes or use the default style 16 | /// object specified. 17 | /// 18 | /// - Parameters: 19 | /// - theAttributes: The attributes to transform. 20 | /// - traitCollection: The trait collection to which to adapt the attributes. 21 | /// - Returns: Attributes with fonts updated to the specified content size category. 22 | public static func adapt(attributes theAttributes: StyleAttributes, to traitCollection: UITraitCollection) -> StyleAttributes { 23 | let adaptations: [AdaptiveStyleTransformation] = EmbeddedTransformationHelpers.transformations(from: theAttributes) 24 | var styleAttributes = theAttributes 25 | for adaptiveStyle in adaptations { 26 | styleAttributes = adaptiveStyle.adapt(attributes: styleAttributes, to: traitCollection) ?? styleAttributes 27 | } 28 | return styleAttributes 29 | } 30 | 31 | /// Create a new `NSAttributedString` adapted to the new trait collection. 32 | /// Re-applies the embedded style objects. 33 | /// 34 | /// - Parameter traitCollection: The trait collection to adapt to. 35 | /// - Returns: A new `NSAttributedString` with the style updated to the new 36 | /// trait collection. 37 | public final func adapted(to traitCollection: UITraitCollection) -> NSAttributedString { 38 | let newString = mutableStringCopy() 39 | newString.beginEditing() 40 | enumerateAttributes(in: NSRange(location: 0, length: length), options: []) { (attributes, range, _) in 41 | var styleAttributes = attributes 42 | 43 | // Adapt any AdaptiveStyleTransformation embedded in the attributes. 44 | let adaptiveStyles: [AdaptiveStyleTransformation] = EmbeddedTransformationHelpers.transformations(from: attributes) 45 | for adaptiveStyle in adaptiveStyles { 46 | styleAttributes = adaptiveStyle.adapt(attributes: styleAttributes, to: traitCollection) ?? styleAttributes 47 | } 48 | // Apply any AttributedStringTransformation embedded in the attributes. 49 | let transformations: [AttributedStringTransformation] = EmbeddedTransformationHelpers.transformations(from: attributes) 50 | for transformation in transformations { 51 | transformation.update(string: newString, in: range) 52 | } 53 | newString.setAttributes(NSAttributedString.adapt(attributes: attributes, to: traitCollection), range: range) 54 | } 55 | newString.endEditing() 56 | return newString 57 | } 58 | 59 | } 60 | 61 | extension StringStyle { 62 | 63 | /// Adapt the receiver's attributes to the provided trait collection. 64 | /// 65 | /// - Parameter traitCollection: The trait collection to adapt to. 66 | /// - Returns: The adapted attributes. 67 | public func attributes(adaptedTo traitCollection: UITraitCollection) -> StyleAttributes { 68 | return NSAttributedString.adapt(attributes: attributes, to: traitCollection) 69 | } 70 | 71 | } 72 | 73 | // MARK: - Deprecations 74 | extension NSAttributedString { 75 | 76 | // Deprecated - search the code and remove other deprecations when you remove this 77 | @available(*, deprecated, renamed: "adapted(to:)") 78 | public final func adapt(to traitCollection: UITraitCollection) -> NSAttributedString { 79 | return adapted(to: traitCollection) 80 | } 81 | 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/UIKit/Tab+Adaptive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tab+Adaptive.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 10/2/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | // Just declare conformance. Implementation is already defined and used even 13 | // if adaptive code is not included in the target. 14 | extension Tab: AttributedStringTransformation { } 15 | 16 | extension Tab: EmbeddedTransformation { 17 | 18 | struct Value { 19 | 20 | static let spacer = "spacer" 21 | static let headIndent = "headIndent" 22 | 23 | } 24 | 25 | static func from(dictionary dict: StyleAttributes) -> EmbeddedTransformation? { 26 | switch (dict[EmbeddedTransformationHelpers.Key.type] as? String, 27 | dict[EmbeddedTransformationHelpers.Key.size] as? CGFloat) { 28 | case (Value.spacer?, let width?): 29 | return Tab.spacer(width) 30 | case (Value.headIndent?, let width?): 31 | return Tab.headIndent(width) 32 | default: 33 | return nil 34 | } 35 | } 36 | 37 | var asDictionary: StyleAttributes { 38 | switch self { 39 | case let .spacer(size): 40 | return [ 41 | EmbeddedTransformationHelpers.Key.type: Value.spacer, 42 | EmbeddedTransformationHelpers.Key.size: size, 43 | ] 44 | 45 | case let .headIndent(size): 46 | return [ 47 | EmbeddedTransformationHelpers.Key.type: Value.headIndent, 48 | EmbeddedTransformationHelpers.Key.size: size, 49 | ] 50 | } 51 | } 52 | 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/UIKit/Tracking+Adaptive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tracking+Adaptive.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 10/2/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | extension Tracking: AdaptiveStyleTransformation { 13 | 14 | func adapt(attributes theAttributes: StyleAttributes, to traitCollection: UITraitCollection) -> StyleAttributes? { 15 | if case .adobe = self { 16 | var attributes = theAttributes 17 | let styledFont = theAttributes[.font] as? UIFont 18 | attributes.update(possibleValue: kerning(for: styledFont), forKey: .kern) 19 | return attributes 20 | } 21 | else { 22 | return nil 23 | } 24 | } 25 | 26 | } 27 | 28 | extension Tracking: EmbeddedTransformation { 29 | 30 | struct Value { 31 | static let adobeTracking = "adobe-tracking" 32 | } 33 | 34 | static func from(dictionary dict: StyleAttributes) -> EmbeddedTransformation? { 35 | if case let (Value.adobeTracking?, size?) = (dict[EmbeddedTransformationHelpers.Key.type] as? String, dict[EmbeddedTransformationHelpers.Key.size] as? CGFloat) { 36 | return Tracking.adobe(size) 37 | } 38 | return nil 39 | } 40 | 41 | var asDictionary: StyleAttributes { 42 | if case let .adobe(size) = self { 43 | return [ 44 | EmbeddedTransformationHelpers.Key.type: Value.adobeTracking, 45 | EmbeddedTransformationHelpers.Key.size: size, 46 | ] 47 | } 48 | else { 49 | // We don't need to persist point tracking, as it does not depend on 50 | // the font size. 51 | return [:] 52 | } 53 | } 54 | 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/UIKit/UIKit+AdaptableTextContainerSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit+AdaptableTextContainerSupport.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 7/19/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | extension UIApplication { 13 | 14 | /// Support for the `AdaptableTextContainer` protocol is enabled with this 15 | /// method. It adds the application as an observer for `UIContentSizeCategoryDidChangeNotification` 16 | /// and floods the change notification to the `UIViewController` hierarchy, 17 | /// which by default floods the view managed by each `UIViewController`. 18 | /// 19 | /// The `UIApplication` delegate is also checked for conformance to 20 | /// `AdaptableTextContainer`, which can be a good place to update appearance 21 | /// proxies and invalidate any hard-wired caches that less responsive code may have. 22 | public final func enableAdaptiveContentSizeMonitor() { 23 | let notificationCenter = NotificationCenter.default 24 | let notificationName = UIContentSizeCategory.didChangeNotification 25 | notificationCenter.addObserver( 26 | self, 27 | selector: #selector(UIApplication.bon_notifyContainedAdaptiveContentSizeContainers(fromNotification:)), 28 | name: notificationName, 29 | object: nil) 30 | } 31 | 32 | // Notify the view controller hierarchy. 33 | @objc internal func bon_notifyContainedAdaptiveContentSizeContainers(fromNotification notification: NSNotification) { 34 | // First notify the app delegate if it conforms to AdaptableTextContainer. 35 | if let container = delegate, let traitCollection = container.window??.traitCollection { 36 | if container.responds(to: #selector(AdaptableTextContainer.adaptText(forTraitCollection:))) { 37 | container.perform(#selector(AdaptableTextContainer.adaptText(forTraitCollection:)), with: traitCollection) 38 | } 39 | } 40 | 41 | for window in windows { 42 | // Notify all views in the view hierarchy 43 | window.notifyContainedAdaptiveContentSizeContainers() 44 | // Notify all of the view controllers 45 | window.rootViewController?.notifyContainedAdaptiveContentSizeContainers() 46 | } 47 | } 48 | 49 | } 50 | 51 | extension UIViewController { 52 | 53 | /// 1. If the view is loaded and not installed in the view hierarchy, notify 54 | /// the receiver's view and subviews. If the view is in the view hierarchy, 55 | /// it has already been notified, so do not notify again. 56 | /// 2. Then notify all child view controllers, then the presented view controller, if any. 57 | final func notifyContainedAdaptiveContentSizeContainers() { 58 | if let view = viewIfLoaded { 59 | if view.window == nil { 60 | view.notifyContainedAdaptiveContentSizeContainers(with: traitCollection) 61 | } 62 | } 63 | for viewController in children { 64 | viewController.notifyContainedAdaptiveContentSizeContainers() 65 | } 66 | presentedViewController?.notifyContainedAdaptiveContentSizeContainers() 67 | adaptText(forTraitCollection: traitCollection) 68 | } 69 | 70 | } 71 | 72 | extension UIView { 73 | 74 | /// Notify any subviews, then notify the receiver if it conforms to `AdaptableTextContainer`. 75 | final func notifyContainedAdaptiveContentSizeContainers(with traitCollection: UITraitCollection? = nil) { 76 | for view in subviews { 77 | view.notifyContainedAdaptiveContentSizeContainers(with: traitCollection ?? self.traitCollection) 78 | } 79 | if responds(to: #selector(AdaptableTextContainer.adaptText(forTraitCollection:))) { 80 | perform(#selector(AdaptableTextContainer.adaptText(forTraitCollection:)), with: traitCollection) 81 | } 82 | } 83 | 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/UIKit/UIKit+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit+Helpers.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/12/16. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) && !os(watchOS) 10 | import UIKit 11 | 12 | // UIKit helpers for iOS and tvOS 13 | 14 | extension UIFont { 15 | 16 | @nonobjc static func bon_preferredFont(forTextStyle textStyle: BonMotTextStyle, compatibleWith traitCollection: UITraitCollection?) -> UIFont { 17 | if #available(iOS 10.0, tvOS 10.0, *) { 18 | return preferredFont(forTextStyle: textStyle, compatibleWith: traitCollection) 19 | } 20 | else { 21 | return preferredFont(forTextStyle: textStyle) 22 | } 23 | } 24 | 25 | /// Retrieve the text style, if it exists, from the font descriptor. 26 | @objc(bon_textStyle) 27 | public final var textStyle: BonMotTextStyle? { 28 | guard let textStyle = fontDescriptor.fontAttributes[UIFontDescriptor.AttributeName.textStyle] as? String else { 29 | return nil 30 | } 31 | return UIFont.TextStyle(rawValue: textStyle) 32 | } 33 | 34 | } 35 | 36 | extension UITraitCollection { 37 | 38 | /// Obtain the `preferredContentSizeCategory` for the trait collection. This 39 | /// is compatible with iOS 9.x and will use the 40 | /// `UIApplication.shared.preferredContentSizeCategory` if the trait collection's 41 | /// `preferredContentSizeCategory` is `UIContentSizeCategory.unspecified`. 42 | public var bon_preferredContentSizeCategory: BonMotContentSizeCategory { 43 | if preferredContentSizeCategory != .unspecified { 44 | return preferredContentSizeCategory 45 | } 46 | return UIScreen.main.traitCollection.preferredContentSizeCategory 47 | } 48 | 49 | } 50 | 51 | extension UIFont { 52 | 53 | /// Uses a font descriptor to return a font with the specified name, but 54 | /// with all other attributes the same as the receiver. 55 | /// 56 | /// - Parameter name: The name of the new font. Use the same name as you 57 | /// would pass to UIFont(name:size:). 58 | /// - Returns: a font with the same attributes as the receiver, but with the 59 | /// the specified name. 60 | final func fontWithSameAttributes(named name: String) -> UIFont { 61 | let descriptor = fontDescriptor.addingAttributes([ 62 | UIFontDescriptor.AttributeName.name: name, 63 | ]) 64 | return UIFont(descriptor: descriptor, size: pointSize) 65 | } 66 | 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Tests/AccessTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTests.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 10/13/17. 6 | // Copyright © 2017 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import XCTest 11 | 12 | class AccessTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | EBGaramondLoader.loadFontIfNeeded() 17 | } 18 | 19 | func testThatThingsThatShouldBePublicArePublic() { 20 | let kernKey = NSAttributedString.Key.bonMotRemovedKernAttribute 21 | // we care more that it's public than that it's equal to this string, 22 | // but might as well test it while we're here. 23 | XCTAssertEqual(kernKey, "com.raizlabs.bonmot.removedKernAttributeRemoved") 24 | 25 | let font = BONFont(name: "EBGaramond12-Regular", size: 24)! 26 | XCTAssertEqual(Tracking.point(10).kerning(for: nil), 10, accuracy: 0.00001) 27 | XCTAssertEqual(Tracking.adobe(100).kerning(for: font), 2.4, accuracy: 0.00001) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Tests/AssertHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssertHelpers.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/1/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import XCTest 11 | 12 | func dataFromImage(image theImage: BONImage) -> Data { 13 | assert(theImage.size != .zero) 14 | #if os(OSX) 15 | let cgImageRef = theImage.cgImage(forProposedRect: nil, context: nil, hints: nil) 16 | let bitmapImageRep = NSBitmapImageRep(cgImage: cgImageRef!) 17 | let pngData = bitmapImageRep.representation(using: .png, properties: [:])! 18 | return pngData 19 | #else 20 | return theImage.pngData()! 21 | #endif 22 | } 23 | 24 | func BONAssert(attributes dictionary: StyleAttributes?, key: NSAttributedString.Key, value: T, file: StaticString = #filePath, line: UInt = #line) { 25 | guard let dictionaryValue = dictionary?[key] as? T else { 26 | XCTFail("value is not of expected type", file: file, line: line) 27 | return 28 | } 29 | XCTAssertEqual(dictionaryValue, value, "\(key): \(dictionaryValue) != \(value)", file: file, line: line) 30 | } 31 | 32 | func BONAssertColor(inAttributes dictionary: StyleAttributes?, key: NSAttributedString.Key, color controlColor: BONColor, file: StaticString = #filePath, line: UInt = #line) { 33 | guard let testColor = dictionary?[key] as? BONColor else { 34 | XCTFail("value is not of expected type", file: file, line: line) 35 | return 36 | } 37 | 38 | let testComps = testColor.rgbaComponents 39 | let controlComps = controlColor.rgbaComponents 40 | 41 | XCTAssertEqual(testComps.r, controlComps.r, accuracy: 0.0001) 42 | XCTAssertEqual(testComps.g, controlComps.g, accuracy: 0.0001) 43 | XCTAssertEqual(testComps.b, controlComps.b, accuracy: 0.0001) 44 | XCTAssertEqual(testComps.a, controlComps.a, accuracy: 0.0001) 45 | } 46 | 47 | func BONAssert(attributes dictionary: StyleAttributes?, key: NSAttributedString.Key, float: T, accuracy: T, file: StaticString = #filePath, line: UInt = #line) where T: FloatingPoint { 48 | guard let dictionaryValue = dictionary?[key] as? T else { 49 | XCTFail("value is not of expected type", file: file, line: line) 50 | return 51 | } 52 | XCTAssertEqual(dictionaryValue, float, accuracy: accuracy, file: file, line: line) 53 | } 54 | 55 | func BONAssert(attributes dictionary: StyleAttributes?, query: (BONFont) -> T, float: T, accuracy: T = T(0.001), file: StaticString = #filePath, line: UInt = #line) where T: BinaryFloatingPoint { 56 | guard let font = dictionary?[.font] as? BONFont else { 57 | XCTFail("value is not of expected type", file: file, line: line) 58 | return 59 | } 60 | let value = query(font) 61 | XCTAssertEqual(value, float, accuracy: accuracy, file: file, line: line) 62 | } 63 | 64 | func BONAssert(attributes dictionary: StyleAttributes?, query: (NSParagraphStyle) -> T, float: T, accuracy: T = T(0.001), file: StaticString = #filePath, line: UInt = #line) where T: BinaryFloatingPoint { 65 | guard let paragraphStyle = dictionary?[.paragraphStyle] as? NSParagraphStyle else { 66 | XCTFail("value is not of expected type", file: file, line: line) 67 | return 68 | } 69 | let actualValue = query(paragraphStyle) 70 | XCTAssertEqual(actualValue, float, accuracy: accuracy, file: file, line: line) 71 | } 72 | 73 | func BONAssert(attributes dictionary: StyleAttributes?, query: (NSParagraphStyle) -> T, value: T, file: StaticString = #filePath, line: UInt = #line) where T: Equatable { 74 | guard let paragraphStyle = dictionary?[.paragraphStyle] as? NSParagraphStyle else { 75 | XCTFail("value is not of expected type", file: file, line: line) 76 | return 77 | } 78 | let actualValue = query(paragraphStyle) 79 | XCTAssertEqual(value, actualValue, file: file, line: line) 80 | } 81 | 82 | func BONAssertEqualImages(_ image1: BONImage, _ image2: BONImage, file: StaticString = #filePath, line: UInt = #line) { 83 | let data1 = dataFromImage(image: image1) 84 | let data2 = dataFromImage(image: image2) 85 | XCTAssertEqual(data1, data2, file: file, line: line) 86 | } 87 | 88 | func BONAssertNotEqualImages(_ image1: BONImage, _ image2: BONImage, file: StaticString = #filePath, line: UInt = #line) { 89 | let data1 = dataFromImage(image: image1) 90 | let data2 = dataFromImage(image: image2) 91 | XCTAssertNotEqual(data1, data2, file: file, line: line) 92 | } 93 | 94 | func BONAssertEqualFonts(_ font1: BONFont, _ font2: BONFont, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { 95 | let descriptor1 = font1.fontDescriptor 96 | let descriptor2 = font2.fontDescriptor 97 | 98 | XCTAssertEqual(descriptor1, descriptor2, message(), file: file, line: line) 99 | } 100 | -------------------------------------------------------------------------------- /Tests/BONFontBehaviorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFontTests.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 7/20/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import XCTest 11 | 12 | #if os(iOS) || os(tvOS) || os(watchOS) 13 | let testTextStyle = UIFont.TextStyle.title3 14 | #endif 15 | 16 | /// Test the platform behavior of [NS|UI]Font 17 | class BONFontBehaviorTests: XCTestCase { 18 | 19 | /// This tests explores how font attributes persist after construction. 20 | /// 21 | /// - note: When a font is created, attributes that are not supported are 22 | /// removed. It appears that font attributes only act as hints as to what 23 | /// features should be enabled in a font, but only if the font supports it. 24 | /// The features that are enabled are still in the font attributes after 25 | /// construction. 26 | func testBONFontDescriptors() { 27 | var attributes = BONFont(name: "Avenir-Roman", size: 10)!.fontDescriptor.fontAttributes 28 | attributes[BONFontDescriptorFeatureSettingsAttribute] = [ 29 | [ 30 | BONFontFeatureTypeIdentifierKey: 1, 31 | BONFontFeatureSelectorIdentifierKey: 1, 32 | ], 33 | ] 34 | #if os(OSX) 35 | let newAttributes = BONFont(descriptor: BONFontDescriptor(fontAttributes: attributes), size: 0)?.fontDescriptor.fontAttributes ?? [:] 36 | #else 37 | let newAttributes = BONFont(descriptor: BONFontDescriptor(fontAttributes: attributes), size: 0).fontDescriptor.fontAttributes 38 | #endif 39 | XCTAssertEqual(newAttributes.count, 2) 40 | XCTAssertEqual(newAttributes["NSFontNameAttribute"] as? String, "Avenir-Roman") 41 | XCTAssertEqual(newAttributes["NSFontSizeAttribute"] as? Int, 10) 42 | } 43 | #if os(iOS) || os(tvOS) || os(watchOS) 44 | 45 | /// Test what happens when a non-standard text style string is supplied. 46 | func testUIFontNewTextStyle() { 47 | var attributes = UIFont(name: "Avenir-Roman", size: 10)!.fontDescriptor.fontAttributes 48 | attributes[UIFontDescriptor.AttributeName.featureSettings] = [ 49 | [ 50 | UIFontDescriptor.FeatureKey.featureIdentifier: 1, 51 | UIFontDescriptor.FeatureKey.typeIdentifier: 1, 52 | ], 53 | ] 54 | attributes[UIFontDescriptor.AttributeName.textStyle] = "Test" 55 | let newAttributes = UIFont(descriptor: UIFontDescriptor(fontAttributes: attributes), size: 0).fontDescriptor.fontAttributes 56 | XCTAssertEqual(newAttributes.count, 2) 57 | XCTAssertEqual(newAttributes["NSFontNameAttribute"] as? String, "Avenir-Roman") 58 | XCTAssertEqual(newAttributes["NSFontSizeAttribute"] as? Int, 10) 59 | } 60 | 61 | /// Demonstrate what happens when a text style feature is added to a 62 | /// non-system font. (It overrides the font.) 63 | func testTextStyleWithOtherFont() { 64 | var attributes = UIFont(name: "Avenir-Roman", size: 10)!.fontDescriptor.fontAttributes 65 | attributes[UIFontDescriptor.AttributeName.textStyle] = testTextStyle 66 | let newAttributes = UIFont(descriptor: UIFontDescriptor(fontAttributes: attributes), size: 0).fontDescriptor.fontAttributes 67 | if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) { 68 | XCTAssertEqual(newAttributes.count, 3) 69 | } 70 | else { 71 | XCTAssertEqual(newAttributes.count, 2) 72 | } 73 | XCTAssertEqual(newAttributes["NSCTFontUIUsageAttribute"] as? BonMotTextStyle, testTextStyle) 74 | XCTAssertEqual(newAttributes["NSFontSizeAttribute"] as? Int, 10) 75 | } 76 | #endif 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Tests/BonMot-OSXTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "0F5DF122-0917-4A54-80A6-27400F440DC7", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "skippedTests" : [ 17 | "FontInspectorTests\/testAvailableFeatures()", 18 | "ImageTintingTests\/testImageTinting()" 19 | ], 20 | "target" : { 21 | "containerPath" : "container:BonMot.xcodeproj", 22 | "identifier" : "ABCD3E271D980E5500273936", 23 | "name" : "BonMot-OSXTests" 24 | } 25 | } 26 | ], 27 | "version" : 1 28 | } 29 | -------------------------------------------------------------------------------- /Tests/BonMot-iOSTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "6ADF4AFA-7D8F-49C5-B1DE-DF5C0BB3F684", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "skippedTests" : [ 17 | "FontInspectorTests\/testAvailableFeatures()", 18 | "FontInspectorTests\/testHasFeature()" 19 | ], 20 | "target" : { 21 | "containerPath" : "container:BonMot.xcodeproj", 22 | "identifier" : "ABCBFD5E1D96E61100FAD37A", 23 | "name" : "BonMot-iOSTests" 24 | } 25 | } 26 | ], 27 | "version" : 1 28 | } 29 | -------------------------------------------------------------------------------- /Tests/BonMot-tvOSTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "95BFCB18-95F2-48B0-96F1-C67F6DA0DE0C", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "skippedTests" : [ 17 | "FontInspectorTests\/testAvailableFeatures()", 18 | "FontInspectorTests\/testHasFeature()" 19 | ], 20 | "target" : { 21 | "containerPath" : "container:BonMot.xcodeproj", 22 | "identifier" : "ABCD3DEE1D96F6E200273936", 23 | "name" : "BonMot-tvOSTests" 24 | } 25 | } 26 | ], 27 | "version" : 1 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Compatibility+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compatibility+Tests.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/13/16. 6 | // Copyright © 2016 Zev Eisenberg. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | 11 | #if os(OSX) 12 | import AppKit 13 | let BONFontDescriptorFeatureSettingsAttribute = NSFontDescriptor.AttributeName.featureSettings 14 | let BONFontFeatureTypeIdentifierKey = NSFontDescriptor.FeatureKey.typeIdentifier 15 | let BONFontFeatureSelectorIdentifierKey = NSFontDescriptor.FeatureKey.selectorIdentifier 16 | typealias BONView = NSView 17 | #else 18 | import UIKit 19 | let BONFontDescriptorFeatureSettingsAttribute = UIFontDescriptor.AttributeName.featureSettings 20 | let BONFontFeatureTypeIdentifierKey = UIFontDescriptor.FeatureKey.featureIdentifier 21 | let BONFontFeatureSelectorIdentifierKey = UIFontDescriptor.FeatureKey.typeIdentifier 22 | typealias BONView = UIView 23 | #endif 24 | 25 | extension NSAttributedString.Key: ExpressibleByStringLiteral { 26 | 27 | public init(stringLiteral value: String) { 28 | self.init(value) 29 | } 30 | 31 | } 32 | 33 | extension BONFontDescriptor.AttributeName: ExpressibleByStringLiteral { 34 | 35 | public init(stringLiteral value: String) { 36 | self.init(rawValue: value) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Tests/EmphasisTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmphasisTests.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 2/12/18. 6 | // Copyright © 2018 Rightpoint. All rights reserved. 7 | // 8 | 9 | @testable import BonMot 10 | import XCTest 11 | 12 | class EmphasisTests: XCTestCase { 13 | 14 | func testEmphasisCombination() { 15 | let baseFont = BONFont.systemFont(ofSize: 20) 16 | let base = StringStyle(.font(baseFont)) 17 | let bold = base.byAdding(.emphasis(.bold)) 18 | let italic = base.byAdding(.emphasis(.italic)) 19 | let combined = bold.byAdding(stringStyle: italic) 20 | let attributes = combined.attributes 21 | guard let font = attributes[.font] as? BONFont else { 22 | XCTFail("Unable to get font") 23 | return 24 | } 25 | 26 | let descriptor = baseFont.fontDescriptor 27 | var traits = descriptor.symbolicTraits 28 | traits.insert([.italic, .bold]) 29 | let newDescriptor: BONFontDescriptor? = descriptor.withSymbolicTraits(traits) 30 | guard let nonNilNewDescriptor = newDescriptor else { 31 | XCTFail("Unable to get descriptor") 32 | return 33 | } 34 | let controlFont = BONFont(descriptor: nonNilNewDescriptor, size: 0) 35 | 36 | XCTAssertEqual(font, controlFont) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Tests/FontInspectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontInspectorTests.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 11/2/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | @testable import BonMot 10 | import XCTest 11 | 12 | class FontInspectorTests: XCTestCase { 13 | 14 | let systemFont = BONFont.systemFont(ofSize: 24) 15 | let garamond: BONFont = { 16 | EBGaramondLoader.loadFontIfNeeded() 17 | return BONFont(name: "EBGaramond12-Regular", size: 24)! 18 | }() 19 | 20 | override func setUp() { 21 | super.setUp() 22 | EBGaramondLoader.loadFontIfNeeded() 23 | } 24 | 25 | func testHasFeature() throws { 26 | XCTAssertTrue(garamond.has(feature: NumberCase.lower)) 27 | try XCTSkipIf(true, "systemFont testing is not consistent") 28 | XCTAssertTrue(systemFont.has(feature: SmallCaps.fromLowercase)) 29 | XCTAssertTrue(systemFont.has(feature: SmallCaps.disabled)) 30 | XCTAssertFalse(systemFont.has(feature: NumberCase.lower)) 31 | } 32 | 33 | /// This test is disabled on macOS because, although it works locally, 34 | /// the font reports _slightly_ different feature availability on the build 35 | /// machine. Perhaps it is installed on the build machine? Possible fix: use 36 | /// CTFontManagerCreateFontDescriptorFromData() to ensure that the copy of 37 | /// EBGaramond12 that is used in the test is definitely the one included in 38 | /// the test bundle. 39 | func testAvailableFeatures() throws { 40 | try XCTSkipIf(true, "This control string is no longer accurate.") 41 | let garamondControlString = [ 42 | "Available font features of EBGaramond12-Regular", 43 | "", 44 | "All Typographic Features", 45 | " Exclusive: false", 46 | " Selectors:", 47 | " * On (default)", 48 | "", 49 | "Ligatures", 50 | " Exclusive: false", 51 | " Selectors:", 52 | " * Common Ligatures (default)", 53 | " * Rare Ligatures", 54 | " * Historical Ligatures", 55 | "", 56 | "Number Spacing", 57 | " Exclusive: true", 58 | " Selectors:", 59 | " * Monospaced Numbers", 60 | " * Proportional Numbers", 61 | " * No Change (default)", 62 | "", 63 | "Vertical Position", 64 | " Exclusive: true", 65 | " Selectors:", 66 | " * Normal Vertical Position (default)", 67 | " * Superiors/Superscripts", 68 | " * Inferiors/Subscripts", 69 | " * Ordinals", 70 | " * Scientific Inferiors", 71 | "", 72 | "Contextual Fractional Forms", 73 | " Exclusive: true", 74 | " Selectors:", 75 | " * No Fractional Forms (default)", 76 | " * Diagonal", 77 | "", 78 | "Number Case", 79 | " Exclusive: true", 80 | " Selectors:", 81 | " * Old-Style Figures", 82 | " * Lining Figures", 83 | " * No Change (default)", 84 | "", 85 | "Text Spacing", 86 | " Exclusive: true", 87 | " Selectors:", 88 | " * No Change (default)", 89 | " * No Kerning", 90 | "", 91 | "Case-Sensitive Layout", 92 | " Exclusive: false", 93 | " Selectors:", 94 | " * Capital Forms", 95 | "", 96 | "Alternative Stylistic Sets", 97 | " Exclusive: false", 98 | " Selectors:", 99 | " * Cyrillic alternate de, el and elj", 100 | " * Stylistic Set 2", 101 | " * Stylistic Set 5", 102 | " * Stylistic Set 6", 103 | " * Stylistic Set 7", 104 | " * Stylistic Set 20", 105 | "", 106 | "Contextual Alternates", 107 | " Exclusive: false", 108 | " Selectors:", 109 | " * Contextual Alternates (default)", 110 | "", 111 | "Lower Case", 112 | " Exclusive: true", 113 | " Selectors:", 114 | " * No Change (default)", 115 | " * Small Capitals", 116 | "", 117 | "Upper Case", 118 | " Exclusive: true", 119 | " Selectors:", 120 | " * No Change (default)", 121 | " * Small Capitals", 122 | ].joined(separator: "\n") 123 | XCTAssertEqual(garamond.availableFontFeatures(includeIdentifiers: false), garamondControlString) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/ImageTintingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTintingTests.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 9/28/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import AppKit 11 | #else 12 | import UIKit 13 | #endif 14 | 15 | @testable import BonMot 16 | import XCTest 17 | 18 | class ImageTintingTests: XCTestCase { 19 | 20 | func logoImage() throws -> BONImage { 21 | #if os(OSX) 22 | let imageForTest = testBundle.image(forResource: "rz-logo-black") 23 | #else 24 | let imageForTest = UIImage(named: "rz-logo-black", in: testBundle, compatibleWith: nil) 25 | #endif 26 | return try XCTUnwrap(imageForTest) 27 | } 28 | 29 | var raizlabsRed: BONColor { 30 | #if os(OSX) 31 | NSColor(deviceRed: 0.92549, green: 0.352941, blue: 0.301961, alpha: 1.0) 32 | #else 33 | UIColor(red: 0.92549, green: 0.352941, blue: 0.301961, alpha: 1.0) 34 | #endif 35 | } 36 | 37 | let accessibilityDescription = "I’m the very model of a modern accessible image." 38 | 39 | func testImageTinting() throws { 40 | #if SWIFT_PACKAGE && os(OSX) 41 | try XCTSkipIf(true, "Doesn't work on macOS SPM targets") 42 | #endif 43 | 44 | let blackImageName = "rz-logo-black" 45 | let redImageName = "rz-logo-red" 46 | 47 | #if os(OSX) 48 | let sourceImage = try XCTUnwrap(testBundle.image(forResource: blackImageName)) 49 | let controlTintedImage = try XCTUnwrap(testBundle.image(forResource: redImageName)) 50 | let testTintedImage = sourceImage.tintedImage(color: raizlabsRed) 51 | #else 52 | let sourceImage = try XCTUnwrap(UIImage(named: blackImageName, in: testBundle, compatibleWith: nil)) 53 | let controlTintedImage = try XCTUnwrap(UIImage(named: redImageName, in: testBundle, compatibleWith: nil)) 54 | let testTintedImage = sourceImage.tintedImage(color: raizlabsRed) 55 | #endif 56 | 57 | BONAssertEqualImages(controlTintedImage, testTintedImage) 58 | } 59 | 60 | func testTintingInAttributedString() throws { 61 | #if os(iOS) || os(tvOS) 62 | try XCTSkipIf(true, "No longer working for iOS/tvOS targets") 63 | #endif 64 | 65 | let imageForTest = try logoImage() 66 | 67 | let untintedString = NSAttributedString.composed(of: [ 68 | imageForTest.styled(with: .color(raizlabsRed)), 69 | ]) 70 | 71 | #if os(OSX) 72 | let tintableImage = imageForTest 73 | tintableImage.isTemplate = true 74 | #else 75 | let tintableImage = imageForTest.withRenderingMode(.alwaysTemplate) 76 | #endif 77 | 78 | let tintedString = NSAttributedString.composed(of: [ 79 | tintableImage.styled(with: .color(raizlabsRed)), 80 | ]) 81 | 82 | let untintedResult = untintedString.snapshotForTesting() 83 | let tintedResult = tintedString.snapshotForTesting() 84 | 85 | XCTAssertNotNil(untintedResult) 86 | XCTAssertNotNil(tintedResult) 87 | 88 | BONAssertNotEqualImages(untintedResult!, tintedResult!) 89 | } 90 | 91 | func testNotTintingInAttributedString() throws { 92 | #if os(iOS) || os(tvOS) 93 | try XCTSkipIf(true, "No longer working for iOS/tvOS targets") 94 | #endif 95 | 96 | let imageForTest = try logoImage() 97 | 98 | let untintedString = NSAttributedString.composed(of: [ 99 | imageForTest, 100 | ]) 101 | 102 | let tintAttemptString = NSAttributedString.composed(of: [ 103 | imageForTest.styled(with: .color(raizlabsRed)), 104 | ]) 105 | 106 | let untintedResult = untintedString.snapshotForTesting() 107 | let tintAttemptResult = tintAttemptString.snapshotForTesting() 108 | 109 | XCTAssertNotNil(untintedResult) 110 | XCTAssertNotNil(tintAttemptResult) 111 | 112 | BONAssertEqualImages(untintedResult!, tintAttemptResult!) 113 | } 114 | 115 | func testAccessibilityIOSAndTVOS() throws { 116 | let imageForTest = try logoImage() 117 | 118 | #if os(iOS) || os(tvOS) 119 | imageForTest.accessibilityLabel = accessibilityDescription 120 | let tintedImage = imageForTest.tintedImage(color: raizlabsRed) 121 | XCTAssertEqual(tintedImage.accessibilityLabel, accessibilityDescription) 122 | XCTAssertEqual(tintedImage.accessibilityLabel, tintedImage.accessibilityLabel) 123 | #endif 124 | } 125 | 126 | func testAccessibilityOSX() throws { 127 | let imageForTest = try logoImage() 128 | 129 | #if os(OSX) 130 | imageForTest.accessibilityDescription = accessibilityDescription 131 | let tintedImage = imageForTest.tintedImage(color: raizlabsRed) 132 | XCTAssertEqual(tintedImage.accessibilityDescription, accessibilityDescription) 133 | XCTAssertEqual(tintedImage.accessibilityDescription, tintedImage.accessibilityDescription) 134 | #endif 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringDebugTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedStringDebugTests.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/1/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | @testable import BonMot 10 | import XCTest 11 | 12 | class NSAttributedStringDebugTests: XCTestCase { 13 | 14 | func robotImage() throws -> BONImage { 15 | #if os(OSX) 16 | let imageForTest = testBundle.image(forResource: "robot") 17 | #else 18 | let imageForTest = UIImage(named: "robot", in: testBundle, compatibleWith: nil) 19 | #endif 20 | return try XCTUnwrap(imageForTest) 21 | } 22 | 23 | func testSpecialFromUnicodeScalar() { 24 | let enDash = Special(rawValue: "\u{2013}") 25 | XCTAssertEqual(enDash, Special.enDash) 26 | } 27 | 28 | func testDebugRepresentationReplacements() { 29 | let testCases: [(String, String)] = [ 30 | ("BonMot", "BonMot"), 31 | ("Bon\tMot", "BonMot"), 32 | ("Bon\nMot", "BonMot"), 33 | ("it ignores spaces", "it ignores spaces"), 34 | ("Pilcrow¶", "Pilcrow¶"), 35 | ("Floppy💾Disk", "Floppy💾Disk"), 36 | ("\u{000A1338}A\u{000A1339}", "A"), 37 | ("neonسلام🚲\u{000A1338}₫\u{000A1339}", "neonسلام🚲"), 38 | ("\n →\t", ""), 39 | ("foo\u{00a0}bar", "foobar"), 40 | ] 41 | for (index, testCase) in testCases.enumerated() { 42 | let line = UInt(#line - testCases.count - 2 + index) 43 | let debugString = NSAttributedString(string: testCase.0).bonMotDebugString 44 | XCTAssertEqual(testCase.1, debugString, line: line) 45 | let fromXML = StringStyle(.xml).attributedString(from: debugString) 46 | // Unassigned unicode replacement is not currently working. No one is actually interested in doing this so I'm going to leave it out. 47 | if !testCase.1.contains("BON:unicode value=") { 48 | XCTAssertEqual(testCase.0, fromXML.string, line: line) 49 | } 50 | } 51 | } 52 | 53 | func testComposedDebugRepresentation() throws { 54 | let imageForTest = try robotImage() 55 | 56 | let testCases: [([Composable], String, UInt)] = [ 57 | ([imageForTest], "", #line), 58 | ([Special.enDash], "", #line), 59 | ([imageForTest, imageForTest], "", #line), 60 | ([Special.enDash, Special.emDash], "", #line), 61 | ([Special.enDash, imageForTest], "", #line), 62 | ([imageForTest, Special.enDash], "", #line), 63 | ([imageForTest, Special.noBreakSpace, "Monday", Special.enDash, "Friday"], "MondayFriday", #line), 64 | ] 65 | for testCase in testCases { 66 | let debugString = NSAttributedString.composed(of: testCase.0).bonMotDebugString 67 | XCTAssertEqual(testCase.1, debugString, line: testCase.2) 68 | } 69 | } 70 | 71 | func testThatNSAttributedStringSpeaksUTF16() { 72 | // We don't actually need to test this - just demonstrating how it works 73 | let string = "\u{000A1338}A" 74 | XCTAssertEqual(string.count, 2) 75 | XCTAssertEqual(string.utf8.count, 5) 76 | XCTAssertEqual(string.utf16.count, 3) 77 | let mutableAttributedString = NSMutableAttributedString(string: string) 78 | XCTAssertEqual(mutableAttributedString.string, string) 79 | mutableAttributedString.replaceCharacters(in: NSRange(location: 0, length: 2), with: "foo") 80 | XCTAssertEqual(mutableAttributedString.string, "fooA") 81 | } 82 | 83 | // ParagraphStyles are a bit interesting, as tabs behave over a line, but multiple paragraph styles can be applied on that line. 84 | // I'm not sure how a multi-paragraph line would behave, but this confirms that NSAttributedString doesn't do any coalescing 85 | func testParagraphStyleBehavior() { 86 | let style1 = NSMutableParagraphStyle() 87 | style1.lineSpacing = 1000 88 | let style2 = NSMutableParagraphStyle() 89 | style2.headIndent = 1000 90 | let string1 = NSMutableAttributedString(string: "first part ", attributes: [.paragraphStyle: style1]) 91 | let string2 = NSAttributedString(string: "second part.\n", attributes: [.paragraphStyle: style2]) 92 | string1.append(string2) 93 | let p1 = string1.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle 94 | let p2 = string1.attribute(.paragraphStyle, at: string1.length - 1, effectiveRange: nil) as? NSParagraphStyle 95 | XCTAssertNotEqual(p1, p2) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Tests/Resources/EBGaramond12-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Tests/Resources/EBGaramond12-Regular.otf -------------------------------------------------------------------------------- /Tests/Resources/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Tests/Resources/robot.png -------------------------------------------------------------------------------- /Tests/Resources/rz-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Tests/Resources/rz-logo-black.png -------------------------------------------------------------------------------- /Tests/Resources/rz-logo-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BonMot/001139aad601ed8009b49a0e868e21df3dea979c/Tests/Resources/rz-logo-red.png -------------------------------------------------------------------------------- /Tests/TextAlignmentConstraintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextAlignmentConstraintTests.swift 3 | // BonMot 4 | // 5 | // Created by Cameron Pulsford on 10/6/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if os(OSX) 12 | import AppKit 13 | #else 14 | import UIKit 15 | #endif 16 | 17 | @testable import BonMot 18 | import XCTest 19 | 20 | class TextAlignmentConstraintTests: XCTestCase { 21 | 22 | private func field(withText text: String, fontSize: CGFloat) -> BONTextField { 23 | let field = BONTextField(frame: .zero) 24 | field.translatesAutoresizingMaskIntoConstraints = false 25 | 26 | field.font = BONFont(name: "Avenir-Roman", size: fontSize) 27 | 28 | #if os(OSX) 29 | field.stringValue = text 30 | #else 31 | field.text = text 32 | #endif 33 | 34 | return field 35 | } 36 | 37 | func testTopConstraint() { 38 | let left = field(withText: "left", fontSize: 17) 39 | let right = field(withText: "right", fontSize: 50) 40 | 41 | let constraint = TextAlignmentConstraint.with( 42 | item: left, 43 | attribute: .top, 44 | relatedBy: .equal, 45 | toItem: right, 46 | attribute: .top 47 | ) 48 | 49 | XCTAssertEqual(constraint.constant, 0, accuracy: 0.0001) 50 | } 51 | 52 | func testCapHeightConstraint() { 53 | let left = field(withText: "left", fontSize: 17) 54 | let right = field(withText: "right", fontSize: 50) 55 | 56 | let constraint = TextAlignmentConstraint.with( 57 | item: left, 58 | attribute: .capHeight, 59 | relatedBy: .equal, 60 | toItem: right, 61 | attribute: .capHeight 62 | ) 63 | 64 | let target: CGFloat = 9.636 65 | 66 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 67 | } 68 | 69 | func testXHeightConstraint() { 70 | let left = field(withText: "left", fontSize: 17) 71 | let right = field(withText: "right", fontSize: 50) 72 | 73 | let constraint = TextAlignmentConstraint.with( 74 | item: left, 75 | attribute: .xHeight, 76 | relatedBy: .equal, 77 | toItem: right, 78 | attribute: .xHeight 79 | ) 80 | 81 | let target: CGFloat = 17.556 82 | 83 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 84 | } 85 | 86 | func testTopToCapHeightConstraint() { 87 | let left = field(withText: "left", fontSize: 17) 88 | let right = field(withText: "right", fontSize: 50) 89 | 90 | let constraint = TextAlignmentConstraint.with( 91 | item: left, 92 | attribute: .top, 93 | relatedBy: .equal, 94 | toItem: right, 95 | attribute: .capHeight 96 | ) 97 | 98 | let target: CGFloat = 14.6 99 | 100 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 101 | } 102 | 103 | func testCapHeightToTopConstraint() { 104 | let left = field(withText: "left", fontSize: 17) 105 | let right = field(withText: "right", fontSize: 50) 106 | 107 | let constraint = TextAlignmentConstraint.with( 108 | item: left, 109 | attribute: .capHeight, 110 | relatedBy: .equal, 111 | toItem: right, 112 | attribute: .top 113 | ) 114 | 115 | let target: CGFloat = -4.964 116 | 117 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 118 | } 119 | 120 | func testTopToXHeightConstraint() { 121 | let left = field(withText: "left", fontSize: 17) 122 | let right = field(withText: "right", fontSize: 50) 123 | 124 | let constraint = TextAlignmentConstraint.with( 125 | item: left, 126 | attribute: .top, 127 | relatedBy: .equal, 128 | toItem: right, 129 | attribute: .xHeight 130 | ) 131 | 132 | let target: CGFloat = 26.6 133 | 134 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 135 | } 136 | 137 | func testXHeightToTopConstraint() { 138 | let left = field(withText: "left", fontSize: 17) 139 | let right = field(withText: "right", fontSize: 50) 140 | 141 | let constraint = TextAlignmentConstraint.with( 142 | item: left, 143 | attribute: .xHeight, 144 | relatedBy: .equal, 145 | toItem: right, 146 | attribute: .top 147 | ) 148 | 149 | let target: CGFloat = -9.044 150 | 151 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 152 | } 153 | 154 | func testCapHeightToXHeightConstraint() { 155 | let left = field(withText: "left", fontSize: 17) 156 | let right = field(withText: "right", fontSize: 50) 157 | 158 | let constraint = TextAlignmentConstraint.with( 159 | item: left, 160 | attribute: .capHeight, 161 | relatedBy: .equal, 162 | toItem: right, 163 | attribute: .xHeight 164 | ) 165 | 166 | let target: CGFloat = 21.636 167 | 168 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 169 | } 170 | 171 | func testXHeightToCapHeightConstraint() { 172 | let left = field(withText: "left", fontSize: 17) 173 | let right = field(withText: "right", fontSize: 50) 174 | 175 | let constraint = TextAlignmentConstraint.with( 176 | item: left, 177 | attribute: .xHeight, 178 | relatedBy: .equal, 179 | toItem: right, 180 | attribute: .capHeight 181 | ) 182 | 183 | let target: CGFloat = 5.556 184 | 185 | XCTAssertEqual(constraint.constant, target, accuracy: 0.0001) 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /Tests/TransformTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformTests.swift 3 | // BonMot 4 | // 5 | // Created by Zev Eisenberg on 3/24/17. 6 | // Copyright © 2017 Rightpoint. All rights reserved. 7 | // 8 | 9 | @testable import BonMot 10 | import XCTest 11 | 12 | private extension Locale { 13 | 14 | static var german: Locale { 15 | return Locale(identifier: "de_DE") 16 | } 17 | 18 | } 19 | 20 | class TransformTests: XCTestCase { 21 | 22 | func testStyle(withTransform theTransform: Transform) -> StringStyle { 23 | return StringStyle( 24 | .color(.darkGray), 25 | .xmlRules([ 26 | .style("bold", StringStyle( 27 | .color(.blue), 28 | .transform(theTransform) 29 | )), 30 | ])) 31 | } 32 | 33 | func assertCorrectColors(inSubstrings substrings: [(substring: String, color: BONColor)], in string: NSAttributedString, file: StaticString = #filePath, line: UInt = #line) { 34 | // Confirm that lengths and strings match 35 | 36 | let reconstructed = substrings.reduce("") { $0 + $1.substring } 37 | XCTAssertEqual(reconstructed, string.string, "Reconstructed string did not match test string", file: file, line: line) 38 | 39 | // Confirm that colors match for substrings 40 | var startIndex = 0 41 | for (substring, substringColor) in substrings { 42 | let substringUTF16Count = substring.utf16.count 43 | defer { startIndex += substringUTF16Count } 44 | 45 | for i in startIndex..<(startIndex + substringUTF16Count) { 46 | let characterRange = NSRange(location: i, length: 1) 47 | let characterAttributes = string.attributes(at: startIndex, longestEffectiveRange: nil, in: characterRange) 48 | 49 | guard let characterColor = characterAttributes[.foregroundColor] as? BONColor else { 50 | XCTFail("Failed to get color at index \(startIndex) of string \(string)", file: file, line: line) 51 | continue 52 | } 53 | 54 | XCTAssertEqual(characterColor, substringColor, "Colors not equal at index \(i) of string \(string)", file: file, line: line) 55 | } 56 | } 57 | } 58 | 59 | func testLowercase() { 60 | let string = "Time remaining: < 1 DAY FROM NOW" 61 | 62 | let styled = string.styled(with: testStyle(withTransform: .lowercase)) 63 | 64 | XCTAssertEqual(styled.string, "Time remaining: < 1 day FROM NOW") 65 | 66 | assertCorrectColors(inSubstrings: [ 67 | ("Time remaining: ", .darkGray), 68 | ("< 1 day", .blue), 69 | (" FROM NOW", .darkGray), 70 | ], in: styled) 71 | } 72 | 73 | func testUppercase() { 74 | let string = "Time remaining: < 1 day from now" 75 | 76 | let styled = string.styled(with: testStyle(withTransform: .uppercase)) 77 | 78 | XCTAssertEqual(styled.string, "Time remaining: < 1 DAY from now") 79 | 80 | assertCorrectColors(inSubstrings: [ 81 | ("Time remaining: ", .darkGray), 82 | ("< 1 DAY", .blue), 83 | (" from now", .darkGray), 84 | ], in: styled) 85 | } 86 | 87 | func testCapitalized() { 88 | let string = "Time remaining: < 1 day after the moment that is now (but no longer)" 89 | 90 | let styled = string.styled(with: testStyle(withTransform: .capitalized)) 91 | 92 | XCTAssertEqual(styled.string, "Time remaining: < 1 Day After The Moment That Is Now (but no longer)") 93 | 94 | assertCorrectColors(inSubstrings: [ 95 | ("Time remaining: ", .darkGray), 96 | ("< 1 Day After The Moment That Is Now", .blue), 97 | (" (but no longer)", .darkGray), 98 | ], in: styled) 99 | } 100 | 101 | func testLocalizedLowercase() { 102 | let string = "Translation: <Straße> is German for street." 103 | 104 | let styled = string.styled(with: testStyle(withTransform: .lowercaseWithLocale(.german))) 105 | 106 | XCTAssertEqual(styled.string, "Translation: is German for street.") 107 | 108 | assertCorrectColors(inSubstrings: [ 109 | ("Translation: ", .darkGray), 110 | ("", .blue), 111 | (" is German for ", .darkGray), 112 | ("street", .blue), 113 | (".", .darkGray), 114 | ], in: styled) 115 | 116 | } 117 | 118 | func testLocalizedUppercase() { 119 | let string = "Translation: <Straße> is German for street." 120 | 121 | let styled = string.styled(with: testStyle(withTransform: .uppercaseWithLocale(.german))) 122 | 123 | XCTAssertEqual(styled.string, "Translation: is German for STREET.") 124 | 125 | assertCorrectColors(inSubstrings: [ 126 | ("Translation: ", .darkGray), 127 | ("", .blue), 128 | (" is German for ", .darkGray), 129 | ("STREET", .blue), 130 | (".", .darkGray), 131 | ], in: styled) 132 | } 133 | 134 | func testLocalizedCapitalized() { 135 | let string = "Translation: <straße> is German for street." 136 | 137 | let styled = string.styled(with: testStyle(withTransform: .capitalizedWithLocale(.german))) 138 | 139 | XCTAssertEqual(styled.string, "Translation: is German for Street.") 140 | 141 | assertCorrectColors(inSubstrings: [ 142 | ("Translation: ", .darkGray), 143 | ("", .blue), 144 | (" is German for ", .darkGray), 145 | ("Street", .blue), 146 | (".", .darkGray), 147 | ], in: styled) 148 | } 149 | 150 | func testCustom() { 151 | let doubler = { (string: String) -> String in 152 | let doubled = string.flatMap { (character: Character) -> [Character] in 153 | switch character { 154 | case " ": return [character] 155 | default: return [character, character] 156 | } 157 | } 158 | let joined = String(doubled) 159 | return joined 160 | } 161 | 162 | XCTAssertEqual(doubler(""), "") 163 | XCTAssertEqual(doubler("a"), "aa") 164 | XCTAssertEqual(doubler("abc"), "aabbcc") 165 | XCTAssertEqual(doubler("abc def"), "aabbcc ddeeff") 166 | XCTAssertEqual(doubler("abc ß def"), "aabbcc ßß ddeeff") 167 | 168 | let string = "Time remaining: < 1 day from now" 169 | 170 | let styled = string.styled(with: testStyle(withTransform: .custom(doubler))) 171 | 172 | XCTAssertEqual(styled.string, "Time remaining: << 11 ddaayy from now") 173 | 174 | assertCorrectColors(inSubstrings: [ 175 | ("Time remaining: ", .darkGray), 176 | ("<< 11 ddaayy", .blue), 177 | (" from now", .darkGray), 178 | ], in: styled) 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /Tests/UIKitBehaviorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitBehaviorTests.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 8/16/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | #if os(iOS) 12 | let defaultTextFieldFontSize: CGFloat = 17 13 | let defaultTextViewFontSize: CGFloat = 12 14 | #elseif os(tvOS) 15 | let defaultTextFieldFontSize: CGFloat = 38 16 | let defaultTextViewFontSize: CGFloat = 38 17 | #endif 18 | 19 | #if canImport(UIKit) 20 | import UIKit 21 | 22 | class UIKitBehaviorTests: XCTestCase { 23 | 24 | func testLabelPropertyBehavior() { 25 | let largeFont = UIFont(name: "Avenir-Roman", size: 20) 26 | let smallFont = UIFont(name: "Avenir-Roman", size: 10) 27 | let label = UILabel() 28 | label.font = largeFont 29 | label.text = "Testing" 30 | 31 | // Ensure font information is mirrored in attributed string 32 | let attributedText = label.attributedText! 33 | let attributeFont = attributedText.attribute(.font, at: 0, effectiveRange: nil) as? UIFont 34 | XCTAssertEqual(attributeFont, largeFont) 35 | 36 | // Change the font in the attributed string 37 | var attributes = attributedText.attributes(at: 0, effectiveRange: nil) 38 | attributes[.font] = smallFont 39 | label.attributedText = NSAttributedString(string: "Testing", attributes: attributes) 40 | // Note that the font property is updated. 41 | XCTAssertEqual(label.font, smallFont) 42 | 43 | if #available(iOS 11, tvOS 11, *) { 44 | // Change the text of the label 45 | label.text = "Testing" 46 | // Note that this does not revert to the original font. The font 47 | // set by the attributed string sticks in iOS 11+ and tvOS 11+. 48 | BONAssertEqualFonts(label.font, smallFont!) 49 | } 50 | else { 51 | // Change the text of the label 52 | label.text = "Testing" 53 | // Note that this reverts to the original font. 54 | BONAssertEqualFonts(label.font, largeFont!) 55 | // When text changes, it updates the font to the last font set to self.font 56 | // The getter for self.font returns the visible font. 57 | } 58 | } 59 | 60 | func testTextFieldFontPropertyBehavior() { 61 | let largeFont = UIFont(name: "Avenir-Roman", size: 20) 62 | let textField = UITextField() 63 | // Note that the font is not nil before the text property is set. 64 | XCTAssertNotNil(textField.font) 65 | XCTAssertEqual(textField.font?.pointSize, defaultTextFieldFontSize) 66 | textField.text = "Testing" 67 | // By default the font is not nil, size 17 (38 on tvOS) (Not 12 as stated in header) 68 | XCTAssertNotNil(textField.font) 69 | XCTAssertEqual(textField.font?.pointSize, defaultTextFieldFontSize) 70 | 71 | textField.font = largeFont 72 | XCTAssertEqual(textField.font?.pointSize, 20) 73 | 74 | // This test breaks on tvOS 11 as of beta 4: http://www.openradar.me/33742507 75 | if #available(tvOS 11.0, *) { 76 | } 77 | else { 78 | textField.font = nil 79 | // Note that font has a default value even though it's optional. 80 | XCTAssertNotNil(textField.font) 81 | XCTAssertEqual(textField.font?.pointSize, defaultTextFieldFontSize) 82 | } 83 | } 84 | 85 | func testTextViewFontPropertyBehavior() { 86 | let largeFont = UIFont(name: "Avenir-Roman", size: 20) 87 | let textField = UITextView() 88 | #if os(iOS) 89 | // Note that the font *is* nil before the text property is set. 90 | XCTAssertNil(textField.font) 91 | #elseif os(tvOS) 92 | // Note that the font size is not nil on tvOS. 93 | XCTAssertNotNil(textField.font) 94 | #endif 95 | textField.text = "Testing" 96 | // By default the font is nil 97 | XCTAssertNotNil(textField.font) 98 | XCTAssertEqual(textField.font?.pointSize, defaultTextViewFontSize) 99 | 100 | textField.font = largeFont 101 | XCTAssertEqual(textField.font?.pointSize, 20) 102 | 103 | textField.font = nil 104 | // Note that font is not re-set like TextField() 105 | XCTAssertNil(textField.font) 106 | } 107 | 108 | func testButtonFontPropertyBehavior() { 109 | let button = UIButton() 110 | 111 | XCTAssertNotNil(button.titleLabel) 112 | XCTAssertNotNil(button.titleLabel?.font) 113 | XCTAssertNil(button.titleLabel?.attributedText) 114 | } 115 | 116 | // Check to see if arbitrary text survives re-configuration (spoiler: it doesn't). 117 | func testLabelAttributedStringAttributePreservationBehavior() { 118 | let label = UILabel() 119 | label.attributedText = NSAttributedString(string: "", attributes: ["TestAttribute": true]) 120 | label.text = "New Text" 121 | label.font = UIFont(name: "Avenir-Roman", size: 10) 122 | let attributes = label.attributedText?.attributes(at: 0, effectiveRange: nil) 123 | XCTAssertNotNil(attributes) 124 | XCTAssertNil(attributes?["TestAttribute"]) 125 | } 126 | 127 | } 128 | 129 | #endif 130 | -------------------------------------------------------------------------------- /Tests/UIKitBonMotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitBonMotTests.swift 3 | // BonMot 4 | // 5 | // Created by Brian King on 9/3/16. 6 | // Copyright © 2016 Rightpoint. All rights reserved. 7 | // 8 | 9 | import BonMot 10 | import XCTest 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | 15 | class UIKitBonMotTests: XCTestCase { 16 | 17 | let expectedFont = adaptiveStyle.font! 18 | 19 | override static func setUp() { 20 | super.setUp() 21 | NamedStyles.shared.registerStyle(forName: "adaptiveStyle", style: adaptiveStyle) 22 | } 23 | func testStyleNameGetters() { 24 | XCTAssertNil(UILabel().bonMotStyleName) 25 | XCTAssertNil(UITextField().bonMotStyleName) 26 | XCTAssertNil(UITextView().bonMotStyleName) 27 | XCTAssertNil(UIButton().bonMotStyleName) 28 | } 29 | 30 | func testLabelExtensions() { 31 | let label = UILabel() 32 | // Make sure the test is valid and the font is different 33 | XCTAssertNotEqual(label.font, expectedFont) 34 | 35 | label.styledText = "." 36 | 37 | // Assign a style by name and ensure the lookup succeeds 38 | label.bonMotStyleName = "adaptiveStyle" 39 | XCTAssertNotNil(label.bonMotStyle) 40 | 41 | XCTAssertEqual(label.styledText, label.text) 42 | XCTAssertEqual((label.attributedText?.attributes(at: 0, effectiveRange: nil)[.font] as? UIFont)!, expectedFont) 43 | BONAssertColor(inAttributes: label.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 44 | 45 | // Update the trait collection and ensure the font grows. 46 | if #available(iOS 10.0, tvOS 10.0, *) { 47 | label.adaptText(forTraitCollection: UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.extraLarge)) 48 | BONAssert(attributes: label.attributedText?.attributes(at: 0, effectiveRange: nil), query: \.pointSize, float: expectedFont.pointSize + 2) 49 | BONAssertColor(inAttributes: label.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 50 | } 51 | } 52 | 53 | func testTextFieldExtensions() { 54 | let textField = UITextField() 55 | // Make sure the test is valid and the font is different 56 | XCTAssertNotEqual(textField.font, expectedFont) 57 | 58 | textField.styledText = "." 59 | 60 | // Assign a style by name and ensure the lookup succeeds 61 | textField.bonMotStyleName = "adaptiveStyle" 62 | XCTAssertNotNil(textField.bonMotStyle) 63 | 64 | XCTAssertEqual(textField.styledText, textField.text) 65 | XCTAssertEqual((textField.attributedText?.attributes(at: 0, effectiveRange: nil)[.font] as? UIFont)!, expectedFont) 66 | BONAssertColor(inAttributes: textField.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 67 | 68 | // Update the trait collection and ensure the font grows. 69 | if #available(iOS 10.0, tvOS 10.0, *) { 70 | textField.adaptText(forTraitCollection: UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.extraLarge)) 71 | BONAssert(attributes: textField.attributedText?.attributes(at: 0, effectiveRange: nil), query: \.pointSize, float: expectedFont.pointSize + 2) 72 | BONAssertColor(inAttributes: textField.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 73 | } 74 | } 75 | 76 | func testTextView() { 77 | let textView = UITextView() 78 | // Make sure the test is valid and the font is different 79 | XCTAssertNotEqual(textView.font, expectedFont) 80 | 81 | textView.styledText = "." 82 | 83 | // Assign a style by name and ensure the lookup succeeds 84 | textView.bonMotStyleName = "adaptiveStyle" 85 | XCTAssertNotNil(textView.bonMotStyle) 86 | 87 | XCTAssertEqual(textView.styledText, textView.text) 88 | XCTAssertEqual(textView.attributedText?.attributes(at: 0, effectiveRange: nil)[.font] as? UIFont, expectedFont) 89 | BONAssertColor(inAttributes: textView.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 90 | 91 | // Update the trait collection and ensure the font grows. 92 | if #available(iOS 10.0, tvOS 10.0, *) { 93 | textView.adaptText(forTraitCollection: UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.extraLarge)) 94 | BONAssert(attributes: textView.attributedText?.attributes(at: 0, effectiveRange: nil), query: \.pointSize, float: expectedFont.pointSize + 2) 95 | BONAssertColor(inAttributes: textView.attributedText?.attributes(at: 0, effectiveRange: nil), key: .foregroundColor, color: adaptiveStyle.color!) 96 | } 97 | } 98 | 99 | func testButton() { 100 | let button = UIButton() 101 | // Make sure the test is valid and the font is different 102 | XCTAssertNotEqual(button.titleLabel?.font, expectedFont) 103 | 104 | button.styledText = "." 105 | 106 | // Assign a style by name and ensure the lookup succeeds 107 | button.bonMotStyleName = "adaptiveStyle" 108 | XCTAssertNotNil(button.bonMotStyle) 109 | 110 | var attributes = button.attributedTitle(for: .normal)?.attributes(at: 0, effectiveRange: nil) 111 | XCTAssertEqual((attributes?[.font] as? UIFont)!, expectedFont) 112 | BONAssertColor(inAttributes: attributes, key: .foregroundColor, color: adaptiveStyle.color!) 113 | 114 | // Update the trait collection and ensure the font grows. 115 | if #available(iOS 10.0, tvOS 10.0, *) { 116 | button.adaptText(forTraitCollection: UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.extraLarge)) 117 | attributes = button.attributedTitle(for: .normal)?.attributes(at: 0, effectiveRange: nil) 118 | BONAssert(attributes: attributes, query: \.pointSize, float: expectedFont.pointSize + 2) 119 | BONAssertColor(inAttributes: attributes, key: .foregroundColor, color: adaptiveStyle.color!) 120 | } 121 | } 122 | 123 | func testSegmentedControl() { 124 | let segmentedControl = UISegmentedControl() 125 | // Make sure the test is valid and the title text attributes are not defined for the normal state 126 | XCTAssertNil(segmentedControl.titleTextAttributes(for: .normal)) 127 | 128 | segmentedControl.insertSegment(withTitle: ".", at: 0, animated: false) 129 | 130 | // Assign the title text attributes for the normal state and ensure original values match 131 | segmentedControl.setTitleTextAttributes(adaptiveStyle.attributes, for: .normal) 132 | 133 | var attributes = segmentedControl.titleTextAttributes(for: .normal) 134 | XCTAssertEqual(attributes?[.font] as? UIFont, expectedFont) 135 | BONAssertColor(inAttributes: attributes, key: .foregroundColor, color: adaptiveStyle.color!) 136 | BONAssert(attributes: attributes, query: { $0.pointSize }, float: expectedFont.pointSize) 137 | 138 | // Update the trait collection and ensure the font grows. 139 | if #available(iOS 10.0, tvOS 10.0, *) { 140 | segmentedControl.adaptText(forTraitCollection: UITraitCollection(preferredContentSizeCategory: UIContentSizeCategory.extraLarge)) 141 | attributes = segmentedControl.titleTextAttributes(for: .normal) 142 | BONAssert(attributes: attributes, query: { $0.pointSize }, float: expectedFont.pointSize + 2) 143 | BONAssertColor(inAttributes: attributes, key: .foregroundColor, color: adaptiveStyle.color!) 144 | } 145 | } 146 | 147 | func writeTestNavigationBar() {} 148 | func writeTestToolbar() {} 149 | func writeTestViewController() {} 150 | func writeTestBarButtonItem() {} 151 | 152 | } 153 | 154 | #endif 155 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | 9 | # Uncomment the line if you want fastlane to automatically update itself 10 | # update_fastlane 11 | 12 | # default_platform(:ios) 13 | fastlane_require 'circleci_artifact' 14 | fastlane_version "2.93.1" 15 | 16 | BUILD_PATH="./build" 17 | DERIVED_DATA_PATH = "#{BUILD_PATH}/derived_data" 18 | PROJECT_NAME='BonMot.xcodeproj' 19 | 20 | desc "Tests & Coverage: iOS, tvOS, macOS. Builds: watchOS." 21 | lane :coverage_all do 22 | bundle_ios = coverage(scheme: "BonMot-iOS", devices: get_devices()) 23 | bundle_tvos = coverage(scheme: "BonMot-tvOS") 24 | # For some reason fastlane tries to build for tvOS simulator unless destination is manually set 25 | bundle_macos = coverage(scheme: "BonMot-OSX", destination: "platform=macOS") 26 | 27 | xchtmlreport(result_bundle_paths: [bundle_ios, bundle_tvos, bundle_macos], 28 | enable_junit: true) 29 | # Unit testing is not available on watchOS 30 | xcodebuild(scheme: "BonMot-watchOS", 31 | derivedDataPath: DERIVED_DATA_PATH) 32 | end 33 | 34 | desc "Tests: iOS, tvOS, macOS. Builds: watchOS." 35 | lane :test_all do 36 | test(scheme: "BonMot-iOS", devices: get_devices()) 37 | test(scheme: "BonMot-OSX", destination: "platform=macOS") 38 | test(scheme: "BonMot-tvOS") 39 | # Unit testing is not available on watchOS 40 | xcodebuild(scheme: "BonMot-watchOS", 41 | derivedDataPath: DERIVED_DATA_PATH) 42 | end 43 | 44 | platform :mac do 45 | desc "Runs Tests & Generates Code Coverage Reports for macOS" 46 | lane :coverage_macos do 47 | coverage(scheme: "BonMot-OSX") 48 | end 49 | 50 | desc "Runs Tests for macOS" 51 | lane :test_macos do 52 | test(scheme: "BonMot-OSX", destination: "platform=macOS") 53 | end 54 | end 55 | 56 | platform :ios do 57 | desc "Runs Tests & Generates Code Coverage Reports for latest iOS" 58 | lane :coverage_ios do 59 | devices = get_devices() 60 | coverage(scheme: "BonMot-iOS", 61 | devices: devices) 62 | end 63 | 64 | desc "Runs Tests for latest iOS" 65 | lane :test_ios do 66 | devices = get_devices() 67 | test(scheme: "BonMot-iOS", 68 | devices: devices) 69 | end 70 | 71 | desc "Runs Tests & Generates Code Coverage Reports for tvOS" 72 | lane :coverage_tvos do 73 | coverage(scheme: "BonMot-tvOS") 74 | end 75 | 76 | desc "Runs Tests for tvOS" 77 | lane :test_tvos do 78 | test(scheme: "BonMot-tvOS") 79 | end 80 | 81 | # Tests cannot be run on watchOS 82 | desc "Build for watchOS" 83 | lane :build_watchos do 84 | xcodebuild(scheme: "BonMot-watchOS", 85 | derivedDataPath: DERIVED_DATA_PATH) 86 | end 87 | end 88 | 89 | def test(scheme:, devices: nil, destination: nil) 90 | # NOTE: Running too many devices concurrently breaks CircleCI resource limits 91 | disable_concurrent_testing = false 92 | if ENV['CIRCLE_BUILD_NUM'] 93 | disable_concurrent_testing = true 94 | end 95 | 96 | xcargs = "" 97 | if !ENV['SWIFT_VERSION'].nil? 98 | xcargs = "SWIFT_VERSION=#{ENV['SWIFT_VERSION']}" 99 | end 100 | 101 | begin 102 | scan( 103 | devices: devices, 104 | destination: destination, 105 | scheme: scheme, 106 | xcargs: xcargs, 107 | derived_data_path: DERIVED_DATA_PATH, 108 | disable_concurrent_testing: disable_concurrent_testing 109 | ) 110 | rescue => ex 111 | # Don't fail the entire lane when running tests, but print failure to STDERR 112 | STDERR.puts ex 113 | end 114 | end 115 | 116 | def coverage(scheme:, devices: nil, destination: nil) 117 | scan_output_path = "#{BUILD_PATH}/#{scheme}/scan" 118 | 119 | # NOTE: Running too many devices concurrently breaks CircleCI resource limits 120 | disable_concurrent_testing = false 121 | if ENV['CIRCLE_BUILD_NUM'] 122 | disable_concurrent_testing = true 123 | end 124 | 125 | begin 126 | scan( 127 | output_types: 'junit,html', 128 | devices: devices, 129 | destination: destination, 130 | scheme: scheme, 131 | output_directory: scan_output_path, 132 | code_coverage: true, 133 | derived_data_path: DERIVED_DATA_PATH, 134 | result_bundle: true, 135 | disable_concurrent_testing: disable_concurrent_testing 136 | ) 137 | rescue => ex 138 | # Don't fail the entire lane when running tests, but print failure to STDERR 139 | STDERR.puts ex 140 | end 141 | 142 | result_bundle_path = Scan.cache[:result_bundle_path] 143 | 144 | # Extract coverage from Xcode 11 xcresult bundle 145 | absolute_result_bundle_path = "#{Dir.pwd}/../#{result_bundle_path}" 146 | absolute_coverage_path = "#{absolute_result_bundle_path}-coverage" 147 | sh("xcparse codecov #{absolute_result_bundle_path} #{absolute_coverage_path}") 148 | xccoverage_files = Dir.glob("#{absolute_coverage_path}/**/action.xccovreport").sort_by { |filename| File.mtime(filename) }.reverse 149 | xccov_file_direct_path = xccoverage_files.first 150 | 151 | slather_use_circleci = "false" 152 | 153 | if ENV['CIRCLE_BUILD_NUM'] 154 | slather_use_circleci = "true" 155 | end 156 | 157 | xcov( 158 | project: PROJECT_NAME, 159 | scheme: scheme, 160 | output_directory: "#{BUILD_PATH}/#{scheme}/xcov", 161 | xccov_file_direct_path: xccov_file_direct_path 162 | ) 163 | 164 | # Add binaries here as you create internal frameworks 165 | slather_binaries = ['BonMot'] 166 | slather_output_directory = "#{BUILD_PATH}/#{scheme}/slather" 167 | 168 | # html and cobertura_xml output must be run separately 169 | slather( 170 | proj: PROJECT_NAME, 171 | scheme: scheme, 172 | binary_basename: slather_binaries, 173 | output_directory: slather_output_directory, 174 | html: "true", 175 | build_directory: DERIVED_DATA_PATH 176 | ) 177 | # Using Cobertura XML allows us to upload to Codecov.io 178 | # Uploading to codecov is handled separately in the .circleci/config.yml 179 | slather( 180 | proj: PROJECT_NAME, 181 | scheme: scheme, 182 | binary_basename: slather_binaries, 183 | output_directory: slather_output_directory, 184 | circleci: slather_use_circleci, 185 | cobertura_xml: "true", 186 | build_directory: DERIVED_DATA_PATH 187 | ) 188 | 189 | xchtmlreport(result_bundle_path: result_bundle_path, 190 | enable_junit: true) 191 | 192 | result_bundle_path 193 | end 194 | 195 | def get_devices() 196 | # The full list of iOS simulators available on CircleCI 197 | # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions 198 | devices = [] 199 | devices.push("iPhone SE") 200 | devices.push("iPhone X") 201 | devices.push("iPhone 11 Pro Max") 202 | devices.push("iPhone 8") 203 | devices.push("iPhone 8 Plus") 204 | devices.push("iPad Pro (10.5-inch)") 205 | devices 206 | end 207 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### coverage_all 17 | 18 | ```sh 19 | [bundle exec] fastlane coverage_all 20 | ``` 21 | 22 | Tests & Coverage: iOS, tvOS, macOS. Builds: watchOS. 23 | 24 | ### test_all 25 | 26 | ```sh 27 | [bundle exec] fastlane test_all 28 | ``` 29 | 30 | Tests: iOS, tvOS, macOS. Builds: watchOS. 31 | 32 | ---- 33 | 34 | 35 | ## Mac 36 | 37 | ### mac coverage_macos 38 | 39 | ```sh 40 | [bundle exec] fastlane mac coverage_macos 41 | ``` 42 | 43 | Runs Tests & Generates Code Coverage Reports for macOS 44 | 45 | ### mac test_macos 46 | 47 | ```sh 48 | [bundle exec] fastlane mac test_macos 49 | ``` 50 | 51 | Runs Tests for macOS 52 | 53 | ---- 54 | 55 | 56 | ## iOS 57 | 58 | ### ios coverage_ios 59 | 60 | ```sh 61 | [bundle exec] fastlane ios coverage_ios 62 | ``` 63 | 64 | Runs Tests & Generates Code Coverage Reports for latest iOS 65 | 66 | ### ios test_ios 67 | 68 | ```sh 69 | [bundle exec] fastlane ios test_ios 70 | ``` 71 | 72 | Runs Tests for latest iOS 73 | 74 | ### ios coverage_tvos 75 | 76 | ```sh 77 | [bundle exec] fastlane ios coverage_tvos 78 | ``` 79 | 80 | Runs Tests & Generates Code Coverage Reports for tvOS 81 | 82 | ### ios test_tvos 83 | 84 | ```sh 85 | [bundle exec] fastlane ios test_tvos 86 | ``` 87 | 88 | Runs Tests for tvOS 89 | 90 | ### ios build_watchos 91 | 92 | ```sh 93 | [bundle exec] fastlane ios build_watchos 94 | ``` 95 | 96 | Build for watchOS 97 | 98 | ---- 99 | 100 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 101 | 102 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 103 | 104 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 105 | -------------------------------------------------------------------------------- /fastlane/actions/xchtmlreport.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | module SharedValues 4 | # XCHTMLREPORT_CUSTOM_VALUE = :XCHTMLREPORT_CUSTOM_VALUE 5 | end 6 | 7 | class XchtmlreportAction < Action 8 | def self.run(params) 9 | result_bundle_path = params[:result_bundle_path] 10 | if result_bundle_path.nil? 11 | result_bundle_path = Scan.cache[:result_bundle_path] 12 | end 13 | result_bundle_paths = params[:result_bundle_paths] 14 | if result_bundle_path and result_bundle_paths.empty? 15 | result_bundle_paths = [result_bundle_path] 16 | end 17 | 18 | if result_bundle_paths.nil? or result_bundle_paths.empty? 19 | UI.user_error!("You must pass at least one result_bundle_path") 20 | end 21 | 22 | binary_path = params[:binary_path] 23 | 24 | if !File.file?(binary_path) 25 | UI.user_error!("xchtmlreport binary not installed! https://github.com/TitouanVanBelle/XCTestHTMLReport") 26 | end 27 | UI.message "Result bundle path: #{result_bundle_path}" 28 | 29 | command = "#{binary_path}" 30 | 31 | result_bundle_paths.each { |path| 32 | command += " -r #{path}" 33 | } 34 | 35 | if params[:enable_junit] 36 | command += " -j" 37 | end 38 | 39 | sh command 40 | 41 | end 42 | 43 | ##################################################### 44 | # @!group Documentation 45 | ##################################################### 46 | 47 | def self.description 48 | "Xcode-like HTML report for Unit and UI Tests" 49 | end 50 | 51 | def self.details 52 | "https://github.com/TitouanVanBelle/XCTestHTMLReport" 53 | end 54 | 55 | def self.available_options 56 | # Define all options your action supports. 57 | 58 | # Below a few examples 59 | [ 60 | FastlaneCore::ConfigItem.new(key: :result_bundle_path, 61 | description: "Path to the result bundle from scan. After running scan you can use Scan.cache[:result_bundle_path]", 62 | conflicting_options: [:result_bundle_paths], 63 | optional: true, 64 | is_string: true, 65 | conflict_block: proc do |value| 66 | UI.user_error!("You can't use 'result_bundle_path' and 'result_bundle_paths' options in one run") 67 | end, 68 | verify_block: proc do |value| 69 | UI.user_error!("Bad path to the result bundle given: #{value}") unless (value and File.directory?(value)) 70 | end), 71 | FastlaneCore::ConfigItem.new(key: :result_bundle_paths, 72 | description: "Array of multiple result bundle paths from scan", 73 | conflicting_options: [:result_bundle_path], 74 | optional: true, 75 | default_value: [], 76 | type: Array, 77 | conflict_block: proc do |value| 78 | UI.user_error!("You can't use 'result_bundle_path' and 'result_bundle_paths' options in one run") 79 | end, 80 | verify_block: proc do |value| 81 | value.each { |path| 82 | UI.user_error!("Bad path to the result bundle given: #{path}") unless (path and File.directory?(path)) 83 | } 84 | end), 85 | FastlaneCore::ConfigItem.new(key: :binary_path, 86 | description: "Path to xchtmlreport binary", 87 | is_string: true, # true: verifies the input is a string, false: every kind of value 88 | default_value: "/usr/local/bin/xchtmlreport"), # the default value if the user didn't provide one 89 | FastlaneCore::ConfigItem.new(key: :enable_junit, 90 | type: Boolean, 91 | default_value: false, 92 | description: "Enables JUnit XML output 'report.junit'", 93 | optional: true), 94 | ] 95 | end 96 | 97 | def self.output 98 | # Define the shared values you are going to provide 99 | # Example 100 | # [ 101 | # ['XCHTMLREPORT_CUSTOM_VALUE', 'A description of what this value contains'] 102 | # ] 103 | end 104 | 105 | def self.return_value 106 | # If your method provides a return value, you can describe here what it does 107 | end 108 | 109 | def self.authors 110 | ["XCTestHTMLReport: TitouanVanBelle", "plugin: chrisballinger"] 111 | end 112 | 113 | def self.is_supported?(platform) 114 | [:ios, :mac].include?(platform) 115 | end 116 | end 117 | end 118 | end 119 | --------------------------------------------------------------------------------