├── .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 |
--------------------------------------------------------------------------------