├── .github └── workflows │ └── main.yml ├── .gitignore ├── .swiftlint.yml ├── CHANGELOG.md ├── Dangerfile ├── DeviceKit.podspec ├── DeviceKit.svg ├── DeviceKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── DeviceKit.xcscheme ├── DeviceKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── Example └── DeviceKitPlayground.playground │ ├── Contents.swift │ ├── contents.xcplayground │ └── playground.xcworkspace │ └── contents.xcworkspacedata ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Scripts └── push.sh ├── Source ├── Device.generated.swift ├── Device.swift.gyb ├── Info.plist └── PrivacyInfo.xcprivacy ├── Tests ├── Info.plist └── Tests.swift └── Utils ├── gyb └── gyb.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | danger: 13 | runs-on: macos-latest 14 | if: github.event_name == 'pull_request' 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | bundler-cache: true 21 | ruby-version: 2.7.8 22 | 23 | - uses: MeilCli/danger-action@v5 24 | with: 25 | plugins_file: Gemfile 26 | install_path: vendor/bundle 27 | danger_file: Dangerfile 28 | danger_id: danger-pr 29 | env: 30 | DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | test: 33 | runs-on: macos-14 34 | 35 | strategy: 36 | matrix: 37 | platform: 38 | - platform=iOS Simulator,name=iPhone 16 Pro Max 39 | - platform=iOS Simulator,name=iPhone 16 Pro 40 | - platform=iOS Simulator,name=iPhone 16 41 | - platform=iOS Simulator,name=iPhone 16 Plus 42 | - platform=iOS Simulator,name=iPhone SE (3rd generation) 43 | - platform=tvOS Simulator,name=Apple TV 44 | - platform=tvOS Simulator,name=Apple TV 4K (3rd generation) 45 | - platform=watchOS Simulator,name=Apple Watch Series 10 (46mm) 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - uses: maxim-lobanov/setup-xcode@v1 51 | with: 52 | xcode-version: latest-stable 53 | 54 | - uses: ruby/setup-ruby@v1 55 | with: 56 | bundler-cache: true 57 | ruby-version: 2.7.8 58 | 59 | - name: xcodebuild 60 | run: set -o pipefail && xcodebuild -scheme DeviceKit -destination "${{ matrix.platform }}" -configuration Debug ONLY_ACTIVE_ARCH=YES -enableCodeCoverage YES test 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/xcode 2 | 3 | ### Xcode ### 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData 11 | *.profraw 12 | *.profdata 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | 25 | ## Other 26 | *.xccheckout 27 | *.moved-aside 28 | *.xcuserstate 29 | *.DS_Store 30 | Utils/gyb.pyc 31 | 32 | .build 33 | /.previous-build 34 | xcuserdata 35 | .DS_Store 36 | *~ 37 | \#* 38 | .\#* 39 | *.xcscmblueprint 40 | /default.profraw 41 | Utilities/Docker/*.tar.gz 42 | .swiftpm 43 | Package.resolved 44 | vendor 45 | 46 | ## Generated Files 47 | # *.generated.swift # we have to check it in because of CocoaPods ... 48 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - file_length 3 | - line_length 4 | - switch_case_alignment 5 | - type_body_length 6 | - identifier_name 7 | 8 | opt_in_rules: 9 | - anyobject_protocol 10 | - array_init 11 | - attributes 12 | - closure_end_indentation 13 | - closure_spacing 14 | - contains_over_first_not_nil 15 | - convenience_type 16 | - empty_count 17 | - empty_string 18 | - empty_xctest_method 19 | - explicit_init 20 | - fallthrough 21 | - first_where 22 | - force_unwrapping 23 | - function_default_parameter_at_end 24 | - inert_defer 25 | - no_extension_access_modifier 26 | - overridden_super_call 27 | - prohibited_super_call 28 | - redundant_nil_coalescing 29 | - vertical_parameter_alignment_on_call 30 | - pattern_matching_keywords 31 | - fatal_error_message 32 | - implicitly_unwrapped_optional 33 | - joined_default_parameter 34 | - let_var_whitespace 35 | - literal_expression_end_indentation 36 | - lower_acl_than_parent 37 | - modifier_order 38 | - multiline_arguments 39 | - multiline_function_chains 40 | - multiline_parameters 41 | - multiple_closures_with_trailing_closure 42 | - nesting 43 | - notification_center_detachment 44 | - object_literal 45 | - operator_usage_whitespace 46 | - override_in_extension 47 | - private_action 48 | - private_outlet 49 | - redundant_type_annotation 50 | - single_test_class 51 | - sorted_imports 52 | - sorted_first_last 53 | - trailing_closure 54 | - unavailable_function 55 | - unneeded_parentheses_in_closure_argument 56 | - yoda_condition 57 | 58 | analyzer_rules: 59 | - unused_import 60 | - unused_private_declaration 61 | 62 | reporter: "xcode" 63 | 64 | identifier_name: 65 | excluded: 66 | - tv 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 5.6.0 4 | 5 | Releasedate: 2025-03-27 6 | 7 | ```ruby 8 | pod 'DeviceKit', '~> 5.6' 9 | ``` 10 | 11 | ### New October 2024 devices 12 | 13 | This version adds support for the devices announced in October 2024: ([#429](https://github.com/devicekit/DeviceKit/pull/429)) 14 | 15 | | Device | Case value | 16 | | --- | --- | 17 | | iPad Mini (A17 Pro) | `Device.iPadMiniA17Pro` | 18 | 19 | ### New February 2025 devices 20 | 21 | This version adds support for the devices announced in February 2025: ([#436](https://github.com/devicekit/DeviceKit/pull/436)) 22 | 23 | | Device | Case value | 24 | | --- | --- | 25 | | iPhone 16e | `Device.iPhone16e` | 26 | 27 | ### New March 2025 devices 28 | 29 | This version adds support for the devices announced in March 2025: ([#436](https://github.com/devicekit/DeviceKit/pull/436)) 30 | 31 | | Device | Case value | 32 | | --- | --- | 33 | | iPad (A16) | `Device.iPadA16` | 34 | | iPad Air 11-inch (M3) | `Device.iPadAir11M3` | 35 | | iPad Air 13-inch (M3) | `Device.iPadAir13M3` | 36 | 37 | ### Fixes 38 | 39 | - Fix iPhone 16 Plus PPI. ([#423](https://github.com/devicekit/DeviceKit/pull/423)) 40 | 41 | ## Version 5.5.0 42 | 43 | Releasedate: 2024-09-22 44 | 45 | ```ruby 46 | pod 'DeviceKit', '~> 5.5' 47 | ``` 48 | 49 | ### New September 2024 devices 50 | 51 | This version adds support for the devices announced at the September 2024 Apple Event: ([#417](https://github.com/devicekit/DeviceKit/pull/417)) 52 | 53 | | Device | Case value | 54 | | --- | --- | 55 | | iPhone 16 | `Device.iPhone16` | 56 | | iPhone 16 Plus | `Device.iPhone16Plus` | 57 | | iPhone 16 Pro | `Device.iPhone16Pro` | 58 | | iPhone 16 Pro Max | `Device.iPhone16ProMax` | 59 | | Apple Watch Series 10 | `Device.appleWatchSeries10_42mm`, `Device.appleWatchSeries10_46mm` | 60 | 61 | ### Fixes 62 | 63 | - Fix incorrect PPI for iPhone 14 Plus and iPhone 15 Plus. ([#418](https://github.com/devicekit/DeviceKit/pull/418)) 64 | 65 | ### Contributors 66 | 67 | Thanks to all the contributers of this release! 68 | - [arindamxd](https://github.com/arindamxd) 69 | 70 | ## Version 5.4.0 71 | 72 | Releasedate: 2024-05-30 73 | 74 | ```ruby 75 | pod 'DeviceKit', '~> 5.4' 76 | ``` 77 | 78 | ### New features 79 | 80 | - Add `Orientation.unknown` for when the device is neither in landscape or portrait orientation. ([#410](https://github.com/devicekit/DeviceKit/pull/410)) 81 | 82 | ### Contributors 83 | 84 | Thanks to all the contributers of this release! 85 | - [GeorgeElsham](https://github.com/GeorgeElsham) 86 | 87 | ## Version 5.3.1 88 | 89 | Releasedate: 2024-05-30 90 | 91 | ```ruby 92 | pod 'DeviceKit', '~> 5.3' 93 | ``` 94 | 95 | ### Bugfixes 96 | 97 | - Process Privacy manifest instead of copy on SPM. ([#409](https://github.com/devicekit/DeviceKit/pull/409)) 98 | 99 | ## Version 5.3.0 100 | 101 | Releasedate: 2024-05-21 102 | 103 | ```ruby 104 | pod 'DeviceKit', '~> 5.3' 105 | ``` 106 | 107 | ### New May 2024 devices 108 | 109 | This version adds support for the devices announced at the May 2024 Apple Event: ([#412](https://github.com/devicekit/DeviceKit/pull/412)) 110 | 111 | | Device | Case value | 112 | | --- | --- | 113 | | iPad Air (11-inch) (M2) | `Device.iPadAir11M2` | 114 | | iPad Air (13-inch) (M2) | `Device.iPadAir13M2` | 115 | | iPad Pro (11-inch) (M4) | `Device.iPadPro11M4` | 116 | | iPad Pro (13-inch) (M4) | `Device.iPadPro13M4` | 117 | 118 | ### New features 119 | 120 | - Added `ApplePencilSupport.firstGenerationUsbC` and `ApplePencilSupport.pro`. ([#412](https://github.com/devicekit/DeviceKit/pull/412)) 121 | 122 | ## Version 5.2.4 123 | 124 | Releasedate: 2024-04-30 125 | 126 | ```ruby 127 | pod 'DeviceKit', '~> 5.2' 128 | ``` 129 | 130 | ### Bugfixes 131 | 132 | - Enable MERGEABLE_LIBRARY flag. ([#407](https://github.com/devicekit/DeviceKit/pull/407)) 133 | 134 | ## Version 5.2.3 135 | 136 | Releasedate: 2024-04-15 137 | 138 | ```ruby 139 | pod 'DeviceKit', '~> 5.2' 140 | ``` 141 | 142 | ### Bugfixes 143 | 144 | - Fix PrivacyInfo for Cocoapods. ([#397](https://github.com/devicekit/DeviceKit/pull/397)) 145 | 146 | ### Contributors 147 | 148 | Thanks to all the contributers of this release! 149 | - [RayJiang16](https://github.com/RayJiang16) 150 | 151 | ## Version 5.2.2 152 | 153 | Releasedate: 2024-02-06 154 | 155 | ```ruby 156 | pod 'DeviceKit', '~> 5.2' 157 | ``` 158 | 159 | ### Bugfixes 160 | 161 | - Fix building SwiftUI previews on macOS properly. ([#389](https://github.com/devicekit/DeviceKit/pull/389)) 162 | - Fix building for visionOS. ([#390](https://github.com/devicekit/DeviceKit/pull/390)) 163 | 164 | ### Contributors 165 | 166 | Thanks to all the contributers of this release! 167 | - [honghaoz](https://github.com/honghaoz) 168 | - [chrisvasselli](https://github.com/chrisvasselli) 169 | 170 | ## Version 5.2.1 171 | 172 | Releasedate: 2024-01-17 173 | 174 | ```ruby 175 | pod 'DeviceKit', '~> 5.2' 176 | ``` 177 | 178 | ### Bugfixes 179 | 180 | - Update swift-tools-version in Package.swift to 5.3 to support Resource files. ([#381](https://github.com/devicekit/DeviceKit/pull/381)) 181 | - Fix PrivacyInfo.xcprivacy not being detected on SPM. ([#384](https://github.com/devicekit/DeviceKit/pull/384)) 182 | 183 | ### Contributors 184 | 185 | Thanks to all the contributers of this release! 186 | - [Semty](https://github.com/Semty) 187 | - Everyone who reported the SPM issues. 188 | 189 | ## Version 5.2.0 190 | 191 | Releasedate: 2024-01-15 192 | 193 | ```ruby 194 | pod 'DeviceKit', '~> 5.2' 195 | ``` 196 | 197 | ### New features 198 | 199 | - Add `.hasUSBCConnectivity` to `Device` to indicate whether the device has an USB-C port. ([#373](https://github.com/devicekit/DeviceKit/pull/373)) 200 | - Add `.hasDynamicIsland` to `Device` to indicate whether the device has a dynamic island. ([#368](https://github.com/devicekit/DeviceKit/pull/368)) 201 | - Add `.has5gSupport` to `Device` to indicate whether the device has 5G support. ([#340](https://github.com/devicekit/DeviceKit/pull/340)) 202 | - Added Privacy Manifest to DeviceKit. ([#367](https://github.com/devicekit/DeviceKit/pull/367)) 203 | - Fix compilation issues when building SwiftUI Previews for macOS. ([#376](https://github.com/devicekit/DeviceKit/pull/376)) 204 | 205 | ### Contributors 206 | 207 | Thanks to all the contributers of this release! 208 | - [mhausherr](https://github.com/mhausherr) 209 | - [417-72KI](https://github.com/417-72KI) 210 | - [furiosFast](https://github.com/furiosFast) 211 | - [DenTelezhkin](https://github.com/DenTelezhkin) 212 | - [honghaoz](https://github.com/honghaoz) 213 | 214 | ## Version 5.1.0 215 | 216 | Releasedate: 2023-09-21 217 | 218 | ```ruby 219 | pod 'DeviceKit', '~> 5.1' 220 | ``` 221 | 222 | ### New September 2023 devices 223 | 224 | This version adds support for the devices announced at the September 2023 Apple Event: ([#360](https://github.com/devicekit/DeviceKit/pull/360), [#361](https://github.com/devicekit/DeviceKit/pull/361)) 225 | 226 | | Device | Case value | 227 | | --- | --- | 228 | | iPhone 15 | `Device.iPhone15` | 229 | | iPhone 15 Plus | `Device.iPhone15Plus` | 230 | | iPhone 15 Pro | `Device.iPhone15Pro` | 231 | | iPhone 15 Pro Max | `Device.iPhone15ProMax` | 232 | | Apple Watch Series 9 | `Device.appleWatchSeries9_41mm`, `Device.appleWatchSeries9_45mm` | 233 | | Apple Watch Ultra 2 | `Device.appleWatchUltra2` | 234 | 235 | ### Fixes 236 | 237 | - Fixes compilation errors that occur when compiling DeviceKit for visionOS. ([#356](https://github.com/devicekit/DeviceKit/pull/356)) 238 | 239 | ### Important notes 240 | 241 | - Note that this version does not add full visionOS support to DeviceKit. It just allows DeviceKit to compile for visionOS. 242 | - When compiling this version of DeviceKit with Xcode 14 or lower, it will produce the following warning 3 times: `Unknown operating system for build configuration 'os'` 243 | 244 | ### Contributors 245 | 246 | Thanks to all the contributers of this release! 247 | - [wo-ist-henry](https://github.com/wo-ist-henry) 248 | 249 | ## Version 5.0.0 250 | 251 | Releasedate: 2022-11-01 252 | 253 | ```ruby 254 | pod 'DeviceKit', '~> 5.0' 255 | ``` 256 | 257 | ### Breaking changes 258 | 259 | - DeviceKit v5.0.0 drops support for the platforms that Xcode 14 also no longer supports: ([#337](https://github.com/devicekit/DeviceKit/pull/337)) 260 | 261 | | Platform | Previous | Now | 262 | |----------|----------|------| 263 | | iOS | 9.0 | 11.0 | 264 | | tvOS | 9.0 | 11.0 | 265 | | watchOS | 2.0 | 4.0 | 266 | 267 | ### New features 268 | 269 | - Retrieve a device's CPU info using eg. `Device.current.cpu`. ([#330](https://github.com/devicekit/DeviceKit/pull/330)) 270 | - Add `.isSimulator` to tvOS and watchOS. ([#245](https://github.com/devicekit/DeviceKit/pull/245)) 271 | - Add `.isCanvas` which indicates whether the app is running in a SwiftUI preview. ([#303](https://github.com/devicekit/DeviceKit/pull/303)) 272 | 273 | ### Contributors 274 | 275 | Thanks to all the contributers of this release! 276 | - [parski](https://github.com/parski) 277 | - [guidev](https://github.com/guidev) 278 | - [JackYoustra](https://github.com/JackYoustra) 279 | 280 | ## Version 4.9.0 281 | 282 | Releasedate: 2022-11-01 283 | 284 | ```ruby 285 | pod 'DeviceKit', '~> 4.9' 286 | ``` 287 | 288 | ### Apple TV 4K (3rd generation) support. 289 | 290 | This version adds support for the new Apple TV (3rd generation) that is released on the 4th of November 2022. ([#335](https://github.com/devicekit/DeviceKit/pull/335)) 291 | 292 | | Device | Case value | 293 | | --- | --- | 294 | | Apple TV 4K (3rd generation) | `Device.appleTV4K3` | 295 | 296 | ### New features 297 | 298 | - Add ability to get current device's thermal state. ([#332](https://github.com/devicekit/DeviceKit/pull/332)) 299 | 300 | ### Bugfixes 301 | 302 | - Fix Apple Watch Series 7 device identifier being incorrect. ([#329](https://github.com/devicekit/DeviceKit/pull/329)) 303 | 304 | ### Contributors 305 | 306 | Thanks to all the contributers of this release! 307 | - [guidev](https://github.com/guidev) 308 | - [chedabob](https://github.com/chedabob) 309 | 310 | ## Version 4.8.0 311 | 312 | Releasedate: 2022-10-28 313 | 314 | ```ruby 315 | pod 'DeviceKit', '~> 4.8' 316 | ``` 317 | 318 | ### New October 2022 devices 319 | 320 | This version adds support for the devices that were released in October 2022: ([#334](https://github.com/devicekit/DeviceKit/pull/334)) 321 | 322 | | Device | Case value | 323 | | --- | --- | 324 | | iPad (10th generation) | `Device.iPad10` | 325 | | iPad Pro 11-inch (4th generation) | `Device.iPadPro11Inch4` | 326 | | iPad Pro 12.9-inch (6th generation) | `Device.iPadPro12Inch6` | 327 | 328 | ## Version 4.7.0 329 | 330 | Releasedate: 2022-09-13 331 | 332 | ```ruby 333 | pod 'DeviceKit', '~> 4.7' 334 | ``` 335 | 336 | ### New September 2022 devices 337 | 338 | This version adds support for the devices announced at the September 2022 Apple Event: ([#324](https://github.com/devicekit/DeviceKit/pull/324)) 339 | 340 | | Device | Case value | 341 | | --- | --- | 342 | | iPhone 14 | `Device.iPhone14` | 343 | | iPhone 14 Plus | `Device.iPhone14Plus` | 344 | | iPhone 14 Pro | `Device.iPhone14Pro` | 345 | | iPhone 14 Pro Max | `Device.iPhone14ProMax` | 346 | | Apple Watch Series 7 (Missing from DeviceKit, from last year) | `Device.appleWatchSeries7_41mm`, `Device.appleWatchSeries7_45mm` | 347 | | Apple Watch Series 8 | `Device.appleWatchSeries8_41mm`, `Device.appleWatchSeries8_45mm` | 348 | | Apple Watch SE (2nd generation) | `Device.appleWatchSE2_40mm`, `Device.appleWatchSE2_44mm` | 349 | | Apple Watch Ultra | `Device.appleWatchUltra` | 350 | 351 | ## Version 4.6.1 352 | 353 | Releasedate: 2022-07-15 354 | 355 | ```ruby 356 | pod 'DeviceKit', '~> 4.6' 357 | ``` 358 | 359 | ### Fixes 360 | 361 | - Fixes Carthage support. ([#288](https://github.com/devicekit/DeviceKit/pull/288)) 362 | 363 | ## Version 4.6.0 364 | 365 | Releasedate: 2022-03-18 366 | 367 | ```ruby 368 | pod 'DeviceKit', '~> 4.6' 369 | ``` 370 | 371 | ### New March 2022 devices 372 | 373 | This version adds support for the devices announced at the March 2022 Apple Event: ([308](https://github.com/devicekit/DeviceKit/pull/308)) 374 | 375 | | Device | Case value | 376 | | --- | --- | 377 | | iPhone SE (3rd generation) | `Device.iPhoneSE3` | 378 | | iPad Air (5th generation) | `Device.iPadAir5` | 379 | 380 | ## Version 4.5.2 381 | 382 | Releasedate: 2021-10-24 383 | 384 | ```ruby 385 | pod 'DeviceKit', '~> 4.5' 386 | ``` 387 | 388 | ### Fixes 389 | 390 | - Fix iPad mini (6th generation) screen size and aspect ratio again. ([#300](https://github.com/devicekit/DeviceKit/pull/300)) 391 | - Add missing device support URLs and images. ([#300](https://github.com/devicekit/DeviceKit/pull/300)) 392 | 393 | ## Version 4.5.1 394 | 395 | Releasedate: 2021-10-15 396 | 397 | ```ruby 398 | pod 'DeviceKit', '~> 4.5' 399 | ``` 400 | 401 | ### Fixes 402 | 403 | - Fix iPad mini (6th generation) screen size and aspect ratio. ([#294](https://github.com/devicekit/DeviceKit/pull/294)) 404 | 405 | ## Version 4.5.0 406 | 407 | Releasedate: 2021-09-16 408 | 409 | ```ruby 410 | pod 'DeviceKit', '~> 4.5' 411 | ``` 412 | 413 | ### New September 2021 devices 414 | 415 | This version adds support for the devices announced at the September 2021 Apple Event: ([#286](https://github.com/devicekit/DeviceKit/pull/286)) 416 | 417 | | Device | Case value | 418 | | --- | --- | 419 | | iPhone 13 | `Device.iPhone13` | 420 | | iPhone 13 mini | `Device.iPhone13Mini` | 421 | | iPhone 13 Pro | `Device.iPhone13Pro` | 422 | | iPhone 13 Pro Max | `Device.iPhone13ProMax` | 423 | | iPad (9th generation) | `Device.iPad9` | 424 | | iPad mini (6th generation) | `Device.iPadMini6` | 425 | 426 | ### Changes 427 | 428 | - Switched from Travis CI to GitHub Actions. 429 | 430 | ## Version 4.4.0 431 | 432 | Releasedate: 2021-04-29 433 | 434 | ```ruby 435 | pod 'DeviceKit', '~> 4.4' 436 | ``` 437 | 438 | This version adds support for the devices announced at the April 2021 Apple Event: ([#279](https://github.com/devicekit/DeviceKit/pull/279)) 439 | 440 | - iPad Pro (11-inch) (3rd generation) `Device.iPadPro11Inch3` 441 | - iPad Pro (12.9-inch) (5th generation) `Device.iPadPro12Inch5` 442 | - Apple TV 4K (2nd generation) `Device.appleTV4K2` 443 | 444 | ## Version 4.3.0 445 | 446 | Releasedate: 2021-02-12 447 | 448 | ```ruby 449 | pod 'DeviceKit', '~> 4.3' 450 | ``` 451 | 452 | This version adds support for the Simulator running on Apple Silicon and fixes documentation: 453 | 454 | - Support for running in Simulator on Apple Silicon. ([#273](https://github.com/devicekit/DeviceKit/pull/273)) 455 | - Fix tech specs link and images for iPhone 12 models and iPad Air (4th generation). ([#272](https://github.com/devicekit/DeviceKit/pull/272)) 456 | 457 | ## Version 4.2.1 458 | 459 | Releasedate: 2020-10-22 460 | 461 | ```ruby 462 | pod 'DeviceKit', '~> 4.2' 463 | ``` 464 | 465 | This version fixes a couple of bugs introduced in the v4.2.0 release: 466 | 467 | - `Device.allDevicesWithALidarSensor` didn't include iPhone 12 Pro and iPhone 12 Pro Max. ([#268](https://github.com/devicekit/DeviceKit/pull/268) [#266](https://github.com/devicekit/DeviceKit/issues/266)) 468 | - `Device.iPadAir4.screenRatio` returned an invalid screen ratio. ([#268](https://github.com/devicekit/DeviceKit/pull/268) [#267](https://github.com/devicekit/DeviceKit/issues/267)) 469 | 470 | ## Version 4.2.0 471 | 472 | Releasedate: 2020-10-21 473 | 474 | ```ruby 475 | pod 'DeviceKit', '~> 4.2' 476 | ``` 477 | 478 | This release will add support for the October 2020 devices. ([#262](https://github.com/devicekit/DeviceKit/pull/262)) 479 | 480 | - iPad Air (4th generation) 481 | - iPhone 12 482 | - iPhone 12 mini 483 | - iPhone 12 Pro 484 | - iPhone 12 Pro Max 485 | ```swift 486 | Device.iPadAir4 487 | 488 | Device.iPhone12 489 | Device.iPhone12Mini 490 | 491 | Device.iPhone12Pro 492 | Device.iPhone12ProMax 493 | ``` 494 | 495 | ## Version 4.1.0 496 | 497 | Releasedate: 2020-09-21 498 | 499 | ```ruby 500 | pod 'DeviceKit', '~> 4.1' 501 | ``` 502 | 503 | This release will add support for the September 2020 devices, which will be released on the 18th of September: ([#256](https://github.com/devicekit/DeviceKit/pull/256)) 504 | - iPad (8th generation) 505 | - Apple Watch Series 6 506 | - Apple Watch SE 507 | ```swift 508 | Device.iPad8 509 | 510 | Device.appleWatchSeries6_40mm 511 | Device.appleWatchSeries6_44mm 512 | 513 | Device.appleWatchSE_40mm 514 | Device.appleWatchSE_44mm 515 | ``` 516 | 517 | Support for iPad Air (4th generation) will be added in a later version since it will be a long time before we know its device identifiers. 518 | 519 | ## Version 4.0.0 520 | 521 | Releasedate: 2020-09-04 522 | 523 | ```ruby 524 | pod 'DeviceKit', '~> 4.0' 525 | ``` 526 | 527 | This is a v4.0.0 release because of the possibly breaking change of no longer supporting iOS 9. This decision was made because of Xcode 12 no longer supporting iOS 8. 528 | 529 | - Dropped support for iOS 8. Lowest supported version is now iOS 9. ([#249](https://github.com/devicekit/DeviceKit/pull/249)) 530 | - Updated project settings for Xcode 12. ([#248](https://github.com/devicekit/DeviceKit/pull/248)) 531 | 532 | ## Version 3.2.0 533 | 534 | Releasedate: 2020-04-29 535 | 536 | ```ruby 537 | pod 'DeviceKit', '~> 3.2' 538 | ``` 539 | 540 | ### iPhone SE (2nd generation) 541 | - Added support for the iPhone SE (2nd generation). ([#238](https://github.com/devicekit/DeviceKit/pull/238)) 542 | ```swift 543 | Device.iPhoneSE2 544 | ``` 545 | 546 | ## Version 3.1.0 547 | 548 | Releasedate: 2020-03-29 549 | 550 | ```ruby 551 | pod 'DeviceKit', '~> 3.1' 552 | ``` 553 | 554 | ### 2020 iPad Pro 555 | - Added support for the new 2020 iPad Pro. ([#235](https://github.com/devicekit/DeviceKit/pull/235)) 556 | ```swift 557 | Device.iPadPro11Inch2 // iPad Pro (11-inch) (2nd generation) 558 | Device.iPadPro12inch4 // iPad Pro (12.9-inch) (4th generation) 559 | ``` 560 | 561 | ### New features 562 | - Added new functions for detecting LiDAR support. 563 | - `Device.allDevicesWithALidarSensor` and `Device.current.hasLidarSensor` 564 | 565 | ## Version 3.0.0 566 | 567 | Releasedate: 2020-01-19 568 | 569 | ```ruby 570 | pod 'DeviceKit', '~> 3.0' 571 | ``` 572 | 573 | ### Breaking changes 574 | - The enum for the Apple TV HD has been renamed from `.appleTV4` to `.appleTVHD`. ([#211](https://github.com/devicekit/DeviceKit/pull/211)) 575 | - `.allSimulatorXSeriesDevices` has been deprecated and replaced by `.allSimulatorDevicesWithSensorHousing`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 576 | - `.allXSeriesDevices` has been deprecated and replaced by `.allDevicesWithSensorHousing`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 577 | 578 | #### Camera 579 | - `CameraTypes` has been renamed to `CameraType`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 580 | - `CameraType.normal` has been deprecated and replaced by `CameraType.wide`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 581 | - `.allDevicesWithNormalCamera` has been deprecated and replaced by `.allDevicesWithWideCamera`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 582 | - `.hasNormalCamera` has been deprecated and replaced by `.hasWideCamera`. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 583 | 584 | ### New features 585 | - You can now check which devices support wireless charging through the following variables: `Device.allDevicesWithWirelessChargingSupport` and `Device.current.supportsWirelessCharging` ([#209](https://github.com/devicekit/DeviceKit/pull/209)) 586 | - New `.safeDescription` variable that will provide you with a safe version of the `.description` variable. ([#212](https://github.com/devicekit/DeviceKit/pull/212)) 587 | - Example: "iPhone Xʀ" vs "iPhone XR" 588 | 589 | ### Bugfixes 590 | - `.allDevicesWith3dTouchSupport` contained `.iPhoneSE` which was incorrect. ([#226](https://github.com/devicekit/DeviceKit/pull/226)) 591 | - Some variables would return incorrect values when running on the simulator. ([#227](https://github.com/devicekit/DeviceKit/pull/227)) 592 | 593 | ## Version 2.3.0 594 | 595 | Releasedate: 2019-10-02 596 | 597 | ```ruby 598 | pod 'DeviceKit', '~> 2.3' 599 | ``` 600 | 601 | ### New devices 602 | - Added support for the new september 2019 devices: 603 | - iPad (7th generation) 604 | 605 | ## Version 2.2.0 606 | 607 | Releasedate: 2019-09-24 608 | 609 | ```ruby 610 | pod 'DeviceKit', '~> 2.2' 611 | ``` 612 | 613 | ### New devices 614 | - Added support for the new september 2019 devices: 615 | - iPhone 11 616 | - iPhone 11 Pro 617 | - iPhone 11 Pro Max 618 | - Apple Watch Series 5 619 | 620 | ### New features 621 | - `Device.current.cameras` now has the `.ultraWide` camera type added for devices with that camera. 622 | 623 | ## Version 2.1.0 624 | 625 | Releasedate: 2019-09-01 626 | 627 | ```ruby 628 | pod 'DeviceKit', '~> 2.1' 629 | ``` 630 | 631 | ### New features 632 | - Add support for the new iPod touch (7th generation) ([#189](https://github.com/devicekit/DeviceKit/pull/189)) 633 | - Added `Device.allApplePencilCapableDevices` and `Device.current.applePencilSupport` variables for checking Apple Pencil support. ([#179](https://github.com/devicekit/DeviceKit/pull/179)) 634 | - `.applePencilSupport` returns `ApplePencilSupport.firstGeneration` or `ApplePencilSupport.secondGeneration` for checking which Apple Pencil is supported. 635 | - Added 3D Touch (iOS) and Force Touch (watchOS) support variables: ([#183](https://github.com/devicekit/DeviceKit/pull/183)) 636 | - iOS 637 | - `Device.allDevicesWith3dTouchSupport` 638 | - `Device.current.has3dTouchSupport` 639 | - watchOS 640 | - `Device.allWatchesWithForceTouchSupport` 641 | - `Device.current.hasForceTouchSupport` 642 | - Added variable to check for the camera's a device has. ([#188](https://github.com/devicekit/DeviceKit/pull/188)) 643 | - Example: `Device.iPhoneXS.cameras` should return `CameraTypes.normal` and `CameraTypes.telephoto`. 644 | 645 | ### Fixes 646 | - Rename iPod touch 5 and 6 to iPod touch (5th generation) and iPod touch (6th generation) respectively. ([#189](https://github.com/devicekit/DeviceKit/pull/189)) 647 | - Rename Apple TV (4th generation) to Apple TV HD to comply with Apple's rename of the device. ([#196](https://github.com/devicekit/DeviceKit/pull/196)) 648 | - Improve support for Swift Package Manager. ([#193](https://github.com/devicekit/DeviceKit/pull/193)) 649 | - Fixed the `Device.current.isZoomed` variable. ([#59 comment](https://github.com/devicekit/DeviceKit/issues/59#issuecomment-519457674) and [#198](https://github.com/devicekit/DeviceKit/pull/198)) 650 | 651 | 652 | ## Version 2.0.0 653 | 654 | Releasedate: 2019-04-10 655 | 656 | ```ruby 657 | pod 'DeviceKit', '~> 2.0' 658 | ``` 659 | 660 | ### Breaking changes 661 | - The original `Device()` constructor has been made private in favour of using `Device.current` to match `UIDevice.current`. 662 | - The enum values for the iPhone Xs, iPhone Xs Max and iPhone Xʀ have been renamed to be `.iPhoneXS`, `.iPhoneXSMax` and `.iPhoneXR` to match proper formatting. 663 | - `.description` for the iPhone Xs, iPhone Xs Max and iPhone Xʀ have been changed to contain small caps formatting for the s and the ʀ part. 664 | - `.description` for the iPad 5 and iPad 6 have been changed to the proper names; iPad (5th generation) and iPad (6th generation). 665 | - `.name`, `.systemName`, `.systemVersion`, `.model`, `.localizedModel`, `.batteryState` and `.batteryLevel` will now all return nil when you try to get its value when the device you are getting it from isn't the current one. (eg. `Device.iPad6.name` while running on iPad 5) 666 | 667 | ### New features 668 | - Updated to Swift 5! 669 | - New `.allDevicesWithRoundedDisplayCorners` and `.hasRoundedDisplayCorners` values to check if a device has rounded display corners. (eg. iPhone Xs and iPad Pro (3rd generation)) 670 | - new `.allDevicesWithSensorHousing` and `.hasSensorHousing` values to check if a device has a screen cutout for the sensor housing. (eg. iPhone Xs) 671 | 672 | ### Bugfixes 673 | - `.isPad` and `.isPhone` are now giving correct outputs again. 674 | 675 | ## Version 1.13.0 (Last Swift 4.2 release) 676 | 677 | Releasedate: 2019-03-29 678 | 679 | ```ruby 680 | pod 'DeviceKit', '~> 1.13' 681 | ``` 682 | 683 | ### New iPads 684 | Added new iPad Mini (5th generation) and iPad Air (3rd generation) 685 | ```swift 686 | Device.iPadMini5 // iPad Mini (5th generation) 687 | Device.iPadAir3 // iPad Air (3rd generation) 688 | ``` 689 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Source: https://gist.github.com/candostdagdeviren/e49271e6a4b80f93f3193af89d10f4b1 2 | 3 | # PR is a work in progress and shouldn't be merged yet 4 | warn "PR is classed as Work in Progress" if github.pr_title.include? "[WIP]" 5 | 6 | # Warn when there is a big PR 7 | warn "Big PR, consider splitting into smaller" if git.lines_of_code > 500 8 | 9 | # Ensure a clean commits history 10 | if git.commits.any? { |c| c.message =~ /^Merge branch '#{github.branch_for_base}'/ } 11 | fail "Please rebase to get rid of the merge commits in this PR" 12 | end 13 | 14 | # Mainly to encourage writing up some reasoning about the PR, rather than 15 | # just leaving a title 16 | if github.pr_body.length < 5 17 | fail "Please provide a summary in the Pull Request description" 18 | end 19 | 20 | # If these are all empty something has gone wrong, better to raise it in a comment 21 | if git.modified_files.empty? && git.added_files.empty? && git.deleted_files.empty? 22 | fail "This PR has no changes at all, this is likely an issue during development." 23 | end 24 | 25 | has_app_changes = !git.modified_files.grep(/ProjectName/).empty? 26 | has_test_changes = !git.modified_files.grep(/ProjectNameTests/).empty? 27 | 28 | # If changes are more than 10 lines of code, tests need to be updated too 29 | if has_app_changes && !has_test_changes && git.lines_of_code > 10 30 | fail("Tests were not updated", sticky: false) 31 | end 32 | 33 | # Info.plist file shouldn't change often. Leave warning if it changes. 34 | is_plist_change = git.modified_files.sort == ["ProjectName/Info.plist"].sort 35 | 36 | if !is_plist_change 37 | warn "Plist changed, don't forget to localize your plist values" 38 | end 39 | 40 | podfile_updated = !git.modified_files.grep(/Podfile/).empty? 41 | 42 | # Leave warning, if Podfile changes 43 | if podfile_updated 44 | warn "The `Podfile` was updated" 45 | end 46 | 47 | # This is swiftlint plugin. More info: https://github.com/ashfurrow/danger-swiftlint 48 | # 49 | # This lints all Swift files and leave comments in PR if 50 | # there is any issue with linting 51 | swiftlint.lint_files 52 | swiftlint.lint_files inline_mode: true 53 | -------------------------------------------------------------------------------- /DeviceKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DeviceKit' 3 | s.version = '5.6.0' 4 | s.summary = 'DeviceKit is a µ-framework that provides a value-type replacement of UIDevice.' 5 | 6 | s.description = <<-DESC 7 | `DeviceKit` is a value-type wrapper and extension of [`UIDevice`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDevice_Class/). It detects both devices and different simulators. 8 | DESC 9 | 10 | s.homepage = 'https://github.com/devicekit/DeviceKit' 11 | s.license = 'MIT' 12 | s.author = 'DeviceKit' 13 | s.social_media_url = 'https://twitter.com/dennis_weissman' 14 | 15 | s.requires_arc = true 16 | s.ios.deployment_target = '13.0' 17 | s.tvos.deployment_target = '13.0' 18 | s.watchos.deployment_target = '7.0' 19 | 20 | s.swift_version = '5.0' 21 | 22 | s.source = { :git => 'https://github.com/devicekit/DeviceKit.git', :tag => s.version } 23 | s.source_files = 'Source/Device.generated.swift' 24 | 25 | s.resource_bundles = { 'DeviceKit' => 'Source/PrivacyInfo.xcprivacy' } 26 | 27 | s.requires_arc = true 28 | end 29 | -------------------------------------------------------------------------------- /DeviceKit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | -------------------------------------------------------------------------------- /DeviceKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 6D7666191F1A083200F59630 /* Run SwiftLint */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 6D76661A1F1A083300F59630 /* Build configuration list for PBXAggregateTarget "Run SwiftLint" */; 13 | buildPhases = ( 14 | 6D76661D1F1A083A00F59630 /* Run SwiftLint */, 15 | ); 16 | dependencies = ( 17 | ); 18 | name = "Run SwiftLint"; 19 | productName = "Run SwiftLint"; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 6D29C0C01F122C7A005B52BD /* Device.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D29C0BF1F122C77005B52BD /* Device.generated.swift */; }; 25 | 955EE5A31D5E581B008C3DA8 /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 955EE59A1D5E581B008C3DA8 /* DeviceKit.framework */; }; 26 | 955EE5B41D5E5A90008C3DA8 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C7E8451C61253900B0189E /* Tests.swift */; }; 27 | FCF1EAD423381E5700B609AA /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = FCF1EAD323381E5700B609AA /* CHANGELOG.md */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 955EE5A41D5E581B008C3DA8 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 95CBDB641BFD2B440065FC66 /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 955EE5991D5E581B008C3DA8; 36 | remoteInfo = DeviceKit; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 6D29C0BC1F122863005B52BD /* Device.swift.gyb */ = {isa = PBXFileReference; explicitFileType = text.script.python; fileEncoding = 4; lineEnding = 0; name = Device.swift.gyb; path = Source/Device.swift.gyb; sourceTree = ""; wrapsLines = 0; }; 42 | 6D29C0BF1F122C77005B52BD /* Device.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Device.generated.swift; path = Source/Device.generated.swift; sourceTree = ""; }; 43 | 6D8442931EED755900F2864D /* Gemfile.lock */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile.lock; sourceTree = ""; }; 44 | 6D9B30FC1F1A2DC50008F7E0 /* gyb.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = gyb.py; sourceTree = ""; }; 45 | 6D9B30FD1F1A2DC50008F7E0 /* gyb */ = {isa = PBXFileReference; lastKnownFileType = text; path = gyb; sourceTree = ""; }; 46 | 6DAAE66C1EDDC53800074892 /* Dangerfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Dangerfile; sourceTree = ""; }; 47 | 6DAAE66D1EDDF06700074892 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = ""; }; 48 | 951E3A0E1C61549400261610 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Source/Info.plist; sourceTree = ""; }; 49 | 954977FA1E748DC600D6FAEB /* push.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = push.sh; path = Scripts/push.sh; sourceTree = ""; }; 50 | 955EE59A1D5E581B008C3DA8 /* DeviceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DeviceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 955EE5A21D5E581B008C3DA8 /* DeviceKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DeviceKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 957D18251C28C1E90067D203 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 53 | 957D18261C28C1F30067D203 /* DeviceKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = DeviceKit.podspec; sourceTree = ""; }; 54 | 95C7E83F1C61239D00B0189E /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 55 | 95C7E8451C61253900B0189E /* Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Tests.swift; path = Tests/Tests.swift; sourceTree = ""; }; 56 | 95C7E8471C612ABA00B0189E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = ""; }; 57 | 95C7E84D1C6130DB00B0189E /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; 58 | 95C7E84E1C61332300B0189E /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 59 | FC5C20FA2A5197E2009406EE /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = ""; }; 60 | FCC5A8F22B5820A6004E159B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Source/PrivacyInfo.xcprivacy; sourceTree = ""; }; 61 | FCF1EAD323381E5700B609AA /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 955EE5961D5E581B008C3DA8 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | 955EE59F1D5E581B008C3DA8 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | 955EE5A31D5E581B008C3DA8 /* DeviceKit.framework in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXFrameworksBuildPhase section */ 81 | 82 | /* Begin PBXGroup section */ 83 | 6D9B30FB1F1A2DC50008F7E0 /* Utils */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 6D9B30FC1F1A2DC50008F7E0 /* gyb.py */, 87 | 6D9B30FD1F1A2DC50008F7E0 /* gyb */, 88 | ); 89 | path = Utils; 90 | sourceTree = ""; 91 | }; 92 | 954977F91E748DB000D6FAEB /* Scripts */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 954977FA1E748DC600D6FAEB /* push.sh */, 96 | ); 97 | name = Scripts; 98 | sourceTree = ""; 99 | }; 100 | 95C7E83B1C6122BF00B0189E /* Source */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 6D29C0BF1F122C77005B52BD /* Device.generated.swift */, 104 | 6D29C0BC1F122863005B52BD /* Device.swift.gyb */, 105 | 951E3A0E1C61549400261610 /* Info.plist */, 106 | FCC5A8F22B5820A6004E159B /* PrivacyInfo.xcprivacy */, 107 | ); 108 | name = Source; 109 | sourceTree = ""; 110 | }; 111 | 95C7E8411C61241200B0189E /* Tests */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 95C7E8451C61253900B0189E /* Tests.swift */, 115 | 95C7E8471C612ABA00B0189E /* Info.plist */, 116 | ); 117 | name = Tests; 118 | sourceTree = ""; 119 | }; 120 | 95CBDB631BFD2B440065FC66 = { 121 | isa = PBXGroup; 122 | children = ( 123 | FC5C20FA2A5197E2009406EE /* .github */, 124 | 95C7E83B1C6122BF00B0189E /* Source */, 125 | 95C7E8411C61241200B0189E /* Tests */, 126 | 954977F91E748DB000D6FAEB /* Scripts */, 127 | 957D18251C28C1E90067D203 /* README.md */, 128 | FCF1EAD323381E5700B609AA /* CHANGELOG.md */, 129 | 95C7E84E1C61332300B0189E /* Package.swift */, 130 | 957D18261C28C1F30067D203 /* DeviceKit.podspec */, 131 | 95C7E84D1C6130DB00B0189E /* .swiftlint.yml */, 132 | 6DAAE66C1EDDC53800074892 /* Dangerfile */, 133 | 6DAAE66D1EDDF06700074892 /* Gemfile */, 134 | 6D8442931EED755900F2864D /* Gemfile.lock */, 135 | 95C7E83F1C61239D00B0189E /* .gitignore */, 136 | 6D9B30FB1F1A2DC50008F7E0 /* Utils */, 137 | 95CBDB701BFD2B5F0065FC66 /* Products */, 138 | ); 139 | indentWidth = 2; 140 | sourceTree = ""; 141 | tabWidth = 2; 142 | usesTabs = 0; 143 | }; 144 | 95CBDB701BFD2B5F0065FC66 /* Products */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 955EE59A1D5E581B008C3DA8 /* DeviceKit.framework */, 148 | 955EE5A21D5E581B008C3DA8 /* DeviceKitTests.xctest */, 149 | ); 150 | name = Products; 151 | sourceTree = ""; 152 | }; 153 | /* End PBXGroup section */ 154 | 155 | /* Begin PBXHeadersBuildPhase section */ 156 | 955EE5971D5E581B008C3DA8 /* Headers */ = { 157 | isa = PBXHeadersBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXHeadersBuildPhase section */ 164 | 165 | /* Begin PBXNativeTarget section */ 166 | 955EE5991D5E581B008C3DA8 /* DeviceKit */ = { 167 | isa = PBXNativeTarget; 168 | buildConfigurationList = 955EE5AB1D5E581B008C3DA8 /* Build configuration list for PBXNativeTarget "DeviceKit" */; 169 | buildPhases = ( 170 | 6D29C0BE1F1228BE005B52BD /* Generate Device */, 171 | 955EE5951D5E581B008C3DA8 /* Sources */, 172 | 955EE5961D5E581B008C3DA8 /* Frameworks */, 173 | 955EE5971D5E581B008C3DA8 /* Headers */, 174 | 955EE5981D5E581B008C3DA8 /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = DeviceKit; 181 | productName = DeviceKit; 182 | productReference = 955EE59A1D5E581B008C3DA8 /* DeviceKit.framework */; 183 | productType = "com.apple.product-type.framework"; 184 | }; 185 | 955EE5A11D5E581B008C3DA8 /* DeviceKitTests */ = { 186 | isa = PBXNativeTarget; 187 | buildConfigurationList = 955EE5AE1D5E581B008C3DA8 /* Build configuration list for PBXNativeTarget "DeviceKitTests" */; 188 | buildPhases = ( 189 | 955EE59E1D5E581B008C3DA8 /* Sources */, 190 | 955EE59F1D5E581B008C3DA8 /* Frameworks */, 191 | 955EE5A01D5E581B008C3DA8 /* Resources */, 192 | ); 193 | buildRules = ( 194 | ); 195 | dependencies = ( 196 | 955EE5A51D5E581B008C3DA8 /* PBXTargetDependency */, 197 | ); 198 | name = DeviceKitTests; 199 | productName = DeviceKitTests; 200 | productReference = 955EE5A21D5E581B008C3DA8 /* DeviceKitTests.xctest */; 201 | productType = "com.apple.product-type.bundle.unit-test"; 202 | }; 203 | /* End PBXNativeTarget section */ 204 | 205 | /* Begin PBXProject section */ 206 | 95CBDB641BFD2B440065FC66 /* Project object */ = { 207 | isa = PBXProject; 208 | attributes = { 209 | LastSwiftUpdateCheck = 0800; 210 | LastUpgradeCheck = 1200; 211 | ORGANIZATIONNAME = "Dennis Weissmann"; 212 | TargetAttributes = { 213 | 6D7666191F1A083200F59630 = { 214 | CreatedOnToolsVersion = 9.0; 215 | }; 216 | 955EE5991D5E581B008C3DA8 = { 217 | CreatedOnToolsVersion = 8.0; 218 | LastSwiftMigration = 1020; 219 | ProvisioningStyle = Automatic; 220 | }; 221 | 955EE5A11D5E581B008C3DA8 = { 222 | CreatedOnToolsVersion = 8.0; 223 | LastSwiftMigration = 1020; 224 | ProvisioningStyle = Automatic; 225 | }; 226 | }; 227 | }; 228 | buildConfigurationList = 95CBDB671BFD2B440065FC66 /* Build configuration list for PBXProject "DeviceKit" */; 229 | compatibilityVersion = "Xcode 9.3"; 230 | developmentRegion = English; 231 | hasScannedForEncodings = 0; 232 | knownRegions = ( 233 | English, 234 | en, 235 | ); 236 | mainGroup = 95CBDB631BFD2B440065FC66; 237 | productRefGroup = 95CBDB701BFD2B5F0065FC66 /* Products */; 238 | projectDirPath = ""; 239 | projectRoot = ""; 240 | targets = ( 241 | 955EE5991D5E581B008C3DA8 /* DeviceKit */, 242 | 955EE5A11D5E581B008C3DA8 /* DeviceKitTests */, 243 | 6D7666191F1A083200F59630 /* Run SwiftLint */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | 955EE5981D5E581B008C3DA8 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | FCF1EAD423381E5700B609AA /* CHANGELOG.md in Resources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | 955EE5A01D5E581B008C3DA8 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXResourcesBuildPhase section */ 265 | 266 | /* Begin PBXShellScriptBuildPhase section */ 267 | 6D29C0BE1F1228BE005B52BD /* Generate Device */ = { 268 | isa = PBXShellScriptBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | ); 272 | inputPaths = ( 273 | "$(SRCROOT)/Source/Device.swift.gyb", 274 | ); 275 | name = "Generate Device"; 276 | outputPaths = ( 277 | "$(SRCROOT)/Source/Device.generated.swift", 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | shellPath = /bin/sh; 281 | shellScript = "./Utils/gyb --line-directive '' -o ./Source/Device.generated.swift ./Source/Device.swift.gyb\n"; 282 | }; 283 | 6D76661D1F1A083A00F59630 /* Run SwiftLint */ = { 284 | isa = PBXShellScriptBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | ); 288 | inputPaths = ( 289 | ); 290 | name = "Run SwiftLint"; 291 | outputPaths = ( 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | shellPath = /bin/sh; 295 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 296 | }; 297 | /* End PBXShellScriptBuildPhase section */ 298 | 299 | /* Begin PBXSourcesBuildPhase section */ 300 | 955EE5951D5E581B008C3DA8 /* Sources */ = { 301 | isa = PBXSourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | 6D29C0C01F122C7A005B52BD /* Device.generated.swift in Sources */, 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | 955EE59E1D5E581B008C3DA8 /* Sources */ = { 309 | isa = PBXSourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | 955EE5B41D5E5A90008C3DA8 /* Tests.swift in Sources */, 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | /* End PBXSourcesBuildPhase section */ 317 | 318 | /* Begin PBXTargetDependency section */ 319 | 955EE5A51D5E581B008C3DA8 /* PBXTargetDependency */ = { 320 | isa = PBXTargetDependency; 321 | target = 955EE5991D5E581B008C3DA8 /* DeviceKit */; 322 | targetProxy = 955EE5A41D5E581B008C3DA8 /* PBXContainerItemProxy */; 323 | }; 324 | /* End PBXTargetDependency section */ 325 | 326 | /* Begin XCBuildConfiguration section */ 327 | 6D76661B1F1A083300F59630 /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | CLANG_ENABLE_OBJC_WEAK = YES; 331 | PRODUCT_NAME = "$(TARGET_NAME)"; 332 | }; 333 | name = Debug; 334 | }; 335 | 6D76661C1F1A083300F59630 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | CLANG_ENABLE_OBJC_WEAK = YES; 339 | PRODUCT_NAME = "$(TARGET_NAME)"; 340 | }; 341 | name = Release; 342 | }; 343 | 955EE5AC1D5E581B008C3DA8 /* Debug */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ALWAYS_SEARCH_USER_PATHS = NO; 347 | APPLICATION_EXTENSION_API_ONLY = YES; 348 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 349 | CLANG_ANALYZER_NONNULL = YES; 350 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 351 | CLANG_CXX_LIBRARY = "libc++"; 352 | CLANG_ENABLE_MODULES = YES; 353 | CLANG_ENABLE_OBJC_ARC = YES; 354 | CLANG_STATIC_ANALYZER_MODE = deep; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 359 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 360 | CODE_SIGN_IDENTITY = ""; 361 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 362 | COPY_PHASE_STRIP = NO; 363 | CURRENT_PROJECT_VERSION = 1; 364 | DEBUG_INFORMATION_FORMAT = dwarf; 365 | DEFINES_MODULE = YES; 366 | DEVELOPMENT_TEAM = ""; 367 | DYLIB_COMPATIBILITY_VERSION = 1; 368 | DYLIB_CURRENT_VERSION = 1; 369 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 370 | GCC_C_LANGUAGE_STANDARD = gnu99; 371 | GCC_DYNAMIC_NO_PIC = NO; 372 | GCC_OPTIMIZATION_LEVEL = 0; 373 | GCC_PREPROCESSOR_DEFINITIONS = ( 374 | "DEBUG=1", 375 | "$(inherited)", 376 | ); 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 379 | INFOPLIST_FILE = Source/Info.plist; 380 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 381 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 382 | LD_RUNPATH_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "@executable_path/Frameworks", 385 | "@loader_path/Frameworks", 386 | ); 387 | MARKETING_VERSION = 5.6.0; 388 | MERGEABLE_LIBRARY = YES; 389 | MTL_ENABLE_DEBUG_INFO = YES; 390 | PRODUCT_BUNDLE_IDENTIFIER = me.dennisweissmann.DeviceKit; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | RUN_CLANG_STATIC_ANALYZER = YES; 393 | SDKROOT = iphoneos; 394 | SKIP_INSTALL = YES; 395 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator watchos watchsimulator xros xrsimulator"; 396 | SUPPORTS_MACCATALYST = YES; 397 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 398 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 399 | SWIFT_VERSION = 5.0; 400 | TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; 401 | TVOS_DEPLOYMENT_TARGET = 13.0; 402 | VERSIONING_SYSTEM = "apple-generic"; 403 | VERSION_INFO_PREFIX = ""; 404 | WATCHOS_DEPLOYMENT_TARGET = 4.0; 405 | }; 406 | name = Debug; 407 | }; 408 | 955EE5AD1D5E581B008C3DA8 /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ALWAYS_SEARCH_USER_PATHS = NO; 412 | APPLICATION_EXTENSION_API_ONLY = YES; 413 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 414 | CLANG_ANALYZER_NONNULL = YES; 415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 416 | CLANG_CXX_LIBRARY = "libc++"; 417 | CLANG_ENABLE_MODULES = YES; 418 | CLANG_ENABLE_OBJC_ARC = YES; 419 | CLANG_STATIC_ANALYZER_MODE = deep; 420 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 421 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 422 | CLANG_WARN_INFINITE_RECURSION = YES; 423 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 424 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 425 | CODE_SIGN_IDENTITY = ""; 426 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 427 | COPY_PHASE_STRIP = NO; 428 | CURRENT_PROJECT_VERSION = 1; 429 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 430 | DEFINES_MODULE = YES; 431 | DEVELOPMENT_TEAM = ""; 432 | DYLIB_COMPATIBILITY_VERSION = 1; 433 | DYLIB_CURRENT_VERSION = 1; 434 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 435 | ENABLE_NS_ASSERTIONS = NO; 436 | GCC_C_LANGUAGE_STANDARD = gnu99; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 439 | INFOPLIST_FILE = Source/Info.plist; 440 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 441 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 442 | LD_RUNPATH_SEARCH_PATHS = ( 443 | "$(inherited)", 444 | "@executable_path/Frameworks", 445 | "@loader_path/Frameworks", 446 | ); 447 | MARKETING_VERSION = 5.6.0; 448 | MERGEABLE_LIBRARY = YES; 449 | MTL_ENABLE_DEBUG_INFO = NO; 450 | PRODUCT_BUNDLE_IDENTIFIER = me.dennisweissmann.DeviceKit; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | RUN_CLANG_STATIC_ANALYZER = YES; 453 | SDKROOT = iphoneos; 454 | SKIP_INSTALL = YES; 455 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator watchos watchsimulator xros xrsimulator"; 456 | SUPPORTS_MACCATALYST = YES; 457 | SWIFT_VERSION = 5.0; 458 | TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; 459 | TVOS_DEPLOYMENT_TARGET = 13.0; 460 | VALIDATE_PRODUCT = YES; 461 | VERSIONING_SYSTEM = "apple-generic"; 462 | VERSION_INFO_PREFIX = ""; 463 | WATCHOS_DEPLOYMENT_TARGET = 4.0; 464 | }; 465 | name = Release; 466 | }; 467 | 955EE5AF1D5E581B008C3DA8 /* Debug */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 471 | ALWAYS_SEARCH_USER_PATHS = NO; 472 | CLANG_ANALYZER_NONNULL = YES; 473 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 474 | CLANG_CXX_LIBRARY = "libc++"; 475 | CLANG_ENABLE_MODULES = YES; 476 | CLANG_ENABLE_OBJC_ARC = YES; 477 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 478 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 479 | CLANG_WARN_INFINITE_RECURSION = YES; 480 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 481 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 482 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 483 | COPY_PHASE_STRIP = NO; 484 | DEBUG_INFORMATION_FORMAT = dwarf; 485 | DEVELOPMENT_TEAM = SWR98KT795; 486 | GCC_C_LANGUAGE_STANDARD = gnu99; 487 | GCC_DYNAMIC_NO_PIC = NO; 488 | GCC_OPTIMIZATION_LEVEL = 0; 489 | GCC_PREPROCESSOR_DEFINITIONS = ( 490 | "DEBUG=1", 491 | "$(inherited)", 492 | ); 493 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 494 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 495 | INFOPLIST_FILE = Tests/Info.plist; 496 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 497 | LD_RUNPATH_SEARCH_PATHS = ( 498 | "$(inherited)", 499 | "@executable_path/Frameworks", 500 | "@loader_path/Frameworks", 501 | ); 502 | MTL_ENABLE_DEBUG_INFO = YES; 503 | PRODUCT_BUNDLE_IDENTIFIER = me.dennisweissmann.DeviceKitTests; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SDKROOT = iphoneos; 506 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 507 | SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; 508 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 509 | SWIFT_VERSION = 5.0; 510 | TVOS_DEPLOYMENT_TARGET = 13.0; 511 | }; 512 | name = Debug; 513 | }; 514 | 955EE5B01D5E581B008C3DA8 /* Release */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 518 | ALWAYS_SEARCH_USER_PATHS = NO; 519 | CLANG_ANALYZER_NONNULL = YES; 520 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 521 | CLANG_CXX_LIBRARY = "libc++"; 522 | CLANG_ENABLE_MODULES = YES; 523 | CLANG_ENABLE_OBJC_ARC = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 526 | CLANG_WARN_INFINITE_RECURSION = YES; 527 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 528 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 529 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 530 | COPY_PHASE_STRIP = NO; 531 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 532 | DEVELOPMENT_TEAM = SWR98KT795; 533 | ENABLE_NS_ASSERTIONS = NO; 534 | GCC_C_LANGUAGE_STANDARD = gnu99; 535 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 536 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 537 | INFOPLIST_FILE = Tests/Info.plist; 538 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 539 | LD_RUNPATH_SEARCH_PATHS = ( 540 | "$(inherited)", 541 | "@executable_path/Frameworks", 542 | "@loader_path/Frameworks", 543 | ); 544 | MTL_ENABLE_DEBUG_INFO = NO; 545 | PRODUCT_BUNDLE_IDENTIFIER = me.dennisweissmann.DeviceKitTests; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | SDKROOT = iphoneos; 548 | SWIFT_COMPILATION_MODE = wholemodule; 549 | SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; 550 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 551 | SWIFT_VERSION = 5.0; 552 | TVOS_DEPLOYMENT_TARGET = 13.0; 553 | VALIDATE_PRODUCT = YES; 554 | }; 555 | name = Release; 556 | }; 557 | 95CBDB681BFD2B440065FC66 /* Debug */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 561 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 562 | CLANG_WARN_BOOL_CONVERSION = YES; 563 | CLANG_WARN_COMMA = YES; 564 | CLANG_WARN_CONSTANT_CONVERSION = YES; 565 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 566 | CLANG_WARN_EMPTY_BODY = YES; 567 | CLANG_WARN_ENUM_CONVERSION = YES; 568 | CLANG_WARN_INFINITE_RECURSION = YES; 569 | CLANG_WARN_INT_CONVERSION = YES; 570 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 571 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 572 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 573 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 574 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 575 | CLANG_WARN_STRICT_PROTOTYPES = YES; 576 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 577 | CLANG_WARN_UNREACHABLE_CODE = YES; 578 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 579 | ENABLE_STRICT_OBJC_MSGSEND = YES; 580 | ENABLE_TESTABILITY = YES; 581 | GCC_NO_COMMON_BLOCKS = YES; 582 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 583 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 584 | GCC_WARN_UNDECLARED_SELECTOR = YES; 585 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 586 | GCC_WARN_UNUSED_FUNCTION = YES; 587 | GCC_WARN_UNUSED_VARIABLE = YES; 588 | LD_RUNPATH_SEARCH_PATHS = ( 589 | "@loader_path/Frameworks", 590 | "@executable_path/Frameworks", 591 | "@loader_path/../Frameworks", 592 | "@executable_path/../Frameworks", 593 | ); 594 | ONLY_ACTIVE_ARCH = YES; 595 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos appletvsimulator watchsimulator watchos"; 596 | SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "compile-time"; 597 | SWIFT_VERSION = 4.2; 598 | }; 599 | name = Debug; 600 | }; 601 | 95CBDB691BFD2B440065FC66 /* Release */ = { 602 | isa = XCBuildConfiguration; 603 | buildSettings = { 604 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 605 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 606 | CLANG_WARN_BOOL_CONVERSION = YES; 607 | CLANG_WARN_COMMA = YES; 608 | CLANG_WARN_CONSTANT_CONVERSION = YES; 609 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 610 | CLANG_WARN_EMPTY_BODY = YES; 611 | CLANG_WARN_ENUM_CONVERSION = YES; 612 | CLANG_WARN_INFINITE_RECURSION = YES; 613 | CLANG_WARN_INT_CONVERSION = YES; 614 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 615 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 616 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 617 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 618 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 619 | CLANG_WARN_STRICT_PROTOTYPES = YES; 620 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 621 | CLANG_WARN_UNREACHABLE_CODE = YES; 622 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 623 | ENABLE_STRICT_OBJC_MSGSEND = YES; 624 | GCC_NO_COMMON_BLOCKS = YES; 625 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 626 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 627 | GCC_WARN_UNDECLARED_SELECTOR = YES; 628 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 629 | GCC_WARN_UNUSED_FUNCTION = YES; 630 | GCC_WARN_UNUSED_VARIABLE = YES; 631 | LD_RUNPATH_SEARCH_PATHS = ( 632 | "@loader_path/Frameworks", 633 | "@executable_path/Frameworks", 634 | "@loader_path/../Frameworks", 635 | "@executable_path/../Frameworks", 636 | ); 637 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos appletvsimulator watchsimulator watchos"; 638 | SWIFT_COMPILATION_MODE = wholemodule; 639 | SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "compile-time"; 640 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 641 | SWIFT_VERSION = 4.2; 642 | }; 643 | name = Release; 644 | }; 645 | /* End XCBuildConfiguration section */ 646 | 647 | /* Begin XCConfigurationList section */ 648 | 6D76661A1F1A083300F59630 /* Build configuration list for PBXAggregateTarget "Run SwiftLint" */ = { 649 | isa = XCConfigurationList; 650 | buildConfigurations = ( 651 | 6D76661B1F1A083300F59630 /* Debug */, 652 | 6D76661C1F1A083300F59630 /* Release */, 653 | ); 654 | defaultConfigurationIsVisible = 0; 655 | defaultConfigurationName = Release; 656 | }; 657 | 955EE5AB1D5E581B008C3DA8 /* Build configuration list for PBXNativeTarget "DeviceKit" */ = { 658 | isa = XCConfigurationList; 659 | buildConfigurations = ( 660 | 955EE5AC1D5E581B008C3DA8 /* Debug */, 661 | 955EE5AD1D5E581B008C3DA8 /* Release */, 662 | ); 663 | defaultConfigurationIsVisible = 0; 664 | defaultConfigurationName = Release; 665 | }; 666 | 955EE5AE1D5E581B008C3DA8 /* Build configuration list for PBXNativeTarget "DeviceKitTests" */ = { 667 | isa = XCConfigurationList; 668 | buildConfigurations = ( 669 | 955EE5AF1D5E581B008C3DA8 /* Debug */, 670 | 955EE5B01D5E581B008C3DA8 /* Release */, 671 | ); 672 | defaultConfigurationIsVisible = 0; 673 | defaultConfigurationName = Release; 674 | }; 675 | 95CBDB671BFD2B440065FC66 /* Build configuration list for PBXProject "DeviceKit" */ = { 676 | isa = XCConfigurationList; 677 | buildConfigurations = ( 678 | 95CBDB681BFD2B440065FC66 /* Debug */, 679 | 95CBDB691BFD2B440065FC66 /* Release */, 680 | ); 681 | defaultConfigurationIsVisible = 0; 682 | defaultConfigurationName = Release; 683 | }; 684 | /* End XCConfigurationList section */ 685 | }; 686 | rootObject = 95CBDB641BFD2B440065FC66 /* Project object */; 687 | } 688 | -------------------------------------------------------------------------------- /DeviceKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DeviceKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DeviceKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DeviceKit.xcodeproj/xcshareddata/xcschemes/DeviceKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 37 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /DeviceKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DeviceKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DeviceKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/DeviceKitPlayground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | // To use this playground, build DeviceKit.framework for any simulator first. 3 | 4 | import DeviceKit 5 | import UIKit 6 | 7 | let device = Device.current 8 | 9 | print(device) // prints, for example, "iPhone 6 Plus" 10 | 11 | /// Get the Device You're Running On 12 | if device == .iPhone6Plus { 13 | // Do something 14 | } else { 15 | // Do something else 16 | } 17 | 18 | /// Get the Device Family 19 | if device.isPod { 20 | // iPods (real or simulator) 21 | } else if device.isPhone { 22 | // iPhone (real or simulator) 23 | } else if device.isPad { 24 | // iPad (real or simulator) 25 | } 26 | 27 | /// Check If Running on Simulator 28 | if device.isSimulator { 29 | // Running on one of the simulators(iPod/iPhone/iPad) 30 | // Skip doing something irrelevant for Simulator 31 | } 32 | 33 | /// Get the Simulator Device 34 | switch device { 35 | case .simulator(.iPhone6s): break // You're running on the iPhone 6s simulator 36 | case .simulator(.iPadAir2): break // You're running on the iPad Air 2 simulator 37 | default: break 38 | } 39 | 40 | /// Make Sure the Device Is Contained in a Preconfigured Group 41 | let groupOfAllowedDevices: [Device] = [.iPhone6, 42 | .iPhone6Plus, 43 | .iPhone6s, 44 | .iPhone6sPlus, 45 | .simulator(.iPhone6), 46 | .simulator(.iPhone6Plus), 47 | .simulator(.iPhone6s), 48 | .simulator(.iPhone6sPlus)] 49 | 50 | if device.isOneOf(groupOfAllowedDevices) { 51 | // Do your action 52 | } 53 | 54 | /// Get the Current Battery State 55 | if let batteryState = device.batteryState, 56 | batteryState == .full || batteryState >= .charging(75) { 57 | print("Your battery is happy! 😊") 58 | } 59 | 60 | /// Get the Current Battery Level 61 | if device.batteryLevel ?? 0 >= 50 { 62 | // install_iOS() 63 | } else { 64 | // showError() 65 | } 66 | 67 | /// Get Low Power mode status 68 | if let batteryState = device.batteryState, 69 | batteryState.lowPowerMode { 70 | print("Low Power mode is enabled! 🔋") 71 | } else { 72 | print("Low Power mode is disabled! 😊") 73 | } 74 | 75 | /// Check if a Guided Access session is currently active 76 | if device.isGuidedAccessSessionActive { 77 | print("Guided Access session is currently active") 78 | } else { 79 | print("No Guided Access session is currently active") 80 | } 81 | 82 | /// Get Screen Brightness 83 | if device.screenBrightness > 50 { 84 | print("Take care of your eyes!") 85 | } 86 | 87 | /// Get Available Disk Space 88 | if Device.volumeAvailableCapacityForOpportunisticUsage ?? 0 > Int64(1_000_000) { 89 | // download that nice-to-have huge file 90 | } 91 | 92 | if Device.volumeAvailableCapacityForImportantUsage ?? 0 > Int64(1_000) { 93 | // download that file you really need 94 | } 95 | 96 | // Get the underlying device 97 | let simulator = Device.simulator(.iPhone8Plus) 98 | let realDevice = Device.iPhone8Plus 99 | simulator.realDevice == realDevice // true 100 | realDevice.realDevice == realDevice // true 101 | 102 | // Get device thermal state 103 | if device.thermalState == .nominal { 104 | print("Thermal state is nominal") 105 | } 106 | -------------------------------------------------------------------------------- /Example/DeviceKitPlayground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Example/DeviceKitPlayground.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'xcpretty' 4 | gem 'cocoapods', '1.15.2' 5 | gem 'danger' 6 | gem 'danger-swiftlint' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.1.3.2) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | mutex_m 17 | tzinfo (~> 2.0) 18 | addressable (2.8.0) 19 | public_suffix (>= 2.0.2, < 5.0) 20 | algoliasearch (1.27.5) 21 | httpclient (~> 2.8, >= 2.8.3) 22 | json (>= 1.5.1) 23 | atomos (0.1.3) 24 | base64 (0.2.0) 25 | bigdecimal (3.1.7) 26 | claide (1.0.3) 27 | claide-plugins (0.9.2) 28 | cork 29 | nap 30 | open4 (~> 1.3) 31 | cocoapods (1.15.2) 32 | addressable (~> 2.8) 33 | claide (>= 1.0.2, < 2.0) 34 | cocoapods-core (= 1.15.2) 35 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 36 | cocoapods-downloader (>= 2.1, < 3.0) 37 | cocoapods-plugins (>= 1.0.0, < 2.0) 38 | cocoapods-search (>= 1.0.0, < 2.0) 39 | cocoapods-trunk (>= 1.6.0, < 2.0) 40 | cocoapods-try (>= 1.1.0, < 2.0) 41 | colored2 (~> 3.1) 42 | escape (~> 0.0.4) 43 | fourflusher (>= 2.3.0, < 3.0) 44 | gh_inspector (~> 1.0) 45 | molinillo (~> 0.8.0) 46 | nap (~> 1.0) 47 | ruby-macho (>= 2.3.0, < 3.0) 48 | xcodeproj (>= 1.23.0, < 2.0) 49 | cocoapods-core (1.15.2) 50 | activesupport (>= 5.0, < 8) 51 | addressable (~> 2.8) 52 | algoliasearch (~> 1.0) 53 | concurrent-ruby (~> 1.1) 54 | fuzzy_match (~> 2.0.4) 55 | nap (~> 1.0) 56 | netrc (~> 0.11) 57 | public_suffix (~> 4.0) 58 | typhoeus (~> 1.0) 59 | cocoapods-deintegrate (1.0.5) 60 | cocoapods-downloader (2.1) 61 | cocoapods-plugins (1.0.0) 62 | nap 63 | cocoapods-search (1.0.1) 64 | cocoapods-trunk (1.6.0) 65 | nap (>= 0.8, < 2.0) 66 | netrc (~> 0.11) 67 | cocoapods-try (1.2.0) 68 | colored2 (3.1.2) 69 | concurrent-ruby (1.2.3) 70 | connection_pool (2.4.1) 71 | cork (0.3.0) 72 | colored2 (~> 3.1) 73 | danger (8.3.1) 74 | claide (~> 1.0) 75 | claide-plugins (>= 0.9.2) 76 | colored2 (~> 3.1) 77 | cork (~> 0.1) 78 | faraday (>= 0.9.0, < 2.0) 79 | faraday-http-cache (~> 2.0) 80 | git (~> 1.7) 81 | kramdown (~> 2.3) 82 | kramdown-parser-gfm (~> 1.0) 83 | no_proxy_fix 84 | octokit (~> 4.7) 85 | terminal-table (>= 1, < 4) 86 | danger-swiftlint (0.26.0) 87 | danger 88 | rake (> 10) 89 | thor (~> 0.19) 90 | drb (2.2.1) 91 | escape (0.0.4) 92 | ethon (0.16.0) 93 | ffi (>= 1.15.0) 94 | faraday (1.5.1) 95 | faraday-em_http (~> 1.0) 96 | faraday-em_synchrony (~> 1.0) 97 | faraday-excon (~> 1.1) 98 | faraday-httpclient (~> 1.0.1) 99 | faraday-net_http (~> 1.0) 100 | faraday-net_http_persistent (~> 1.1) 101 | faraday-patron (~> 1.0) 102 | multipart-post (>= 1.2, < 3) 103 | ruby2_keywords (>= 0.0.4) 104 | faraday-em_http (1.0.0) 105 | faraday-em_synchrony (1.0.0) 106 | faraday-excon (1.1.0) 107 | faraday-http-cache (2.2.0) 108 | faraday (>= 0.8) 109 | faraday-httpclient (1.0.1) 110 | faraday-net_http (1.0.1) 111 | faraday-net_http_persistent (1.2.0) 112 | faraday-patron (1.0.0) 113 | ffi (1.16.3) 114 | fourflusher (2.3.1) 115 | fuzzy_match (2.0.4) 116 | gh_inspector (1.1.3) 117 | git (1.9.1) 118 | rchardet (~> 1.8) 119 | httpclient (2.8.3) 120 | i18n (1.14.4) 121 | concurrent-ruby (~> 1.0) 122 | json (2.7.2) 123 | kramdown (2.3.1) 124 | rexml 125 | kramdown-parser-gfm (1.1.0) 126 | kramdown (~> 2.0) 127 | minitest (5.22.3) 128 | molinillo (0.8.0) 129 | multipart-post (2.1.1) 130 | mutex_m (0.2.0) 131 | nanaimo (0.3.0) 132 | nap (1.1.0) 133 | netrc (0.11.0) 134 | nkf (0.2.0) 135 | no_proxy_fix (0.1.2) 136 | octokit (4.21.0) 137 | faraday (>= 0.9) 138 | sawyer (~> 0.8.0, >= 0.5.3) 139 | open4 (1.3.4) 140 | public_suffix (4.0.6) 141 | rake (13.0.6) 142 | rchardet (1.8.0) 143 | rexml (3.2.5) 144 | rouge (2.0.7) 145 | ruby-macho (2.5.1) 146 | ruby2_keywords (0.0.4) 147 | sawyer (0.8.2) 148 | addressable (>= 2.3.5) 149 | faraday (> 0.8, < 2.0) 150 | terminal-table (3.0.1) 151 | unicode-display_width (>= 1.1.1, < 3) 152 | thor (0.20.3) 153 | typhoeus (1.4.1) 154 | ethon (>= 0.9.0) 155 | tzinfo (2.0.6) 156 | concurrent-ruby (~> 1.0) 157 | unicode-display_width (2.0.0) 158 | xcodeproj (1.24.0) 159 | CFPropertyList (>= 2.3.3, < 4.0) 160 | atomos (~> 0.1.3) 161 | claide (>= 1.0.2, < 2.0) 162 | colored2 (~> 3.1) 163 | nanaimo (~> 0.3.0) 164 | rexml (~> 3.2.4) 165 | xcpretty (0.3.0) 166 | rouge (~> 2.0.7) 167 | 168 | PLATFORMS 169 | ruby 170 | 171 | DEPENDENCIES 172 | cocoapods (= 1.15.2) 173 | danger 174 | danger-swiftlint 175 | xcpretty 176 | 177 | BUNDLED WITH 178 | 2.1.4 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Dennis Weissmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the DeviceKit open source project 5 | // 6 | // Copyright © 2014 - 2018 Dennis Weissmann and the DeviceKit project authors 7 | // 8 | // License: https://github.com/dennisweissmann/DeviceKit/blob/master/LICENSE 9 | // Contributors: https://github.com/dennisweissmann/DeviceKit#contributors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | import PackageDescription 14 | 15 | let package = Package( 16 | name: "DeviceKit", 17 | platforms: [ 18 | .iOS(.v13), 19 | .tvOS(.v13), 20 | .watchOS(.v4) 21 | ], 22 | products: [ 23 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 24 | .library( 25 | name: "DeviceKit", 26 | targets: ["DeviceKit"] 27 | ) 28 | ], 29 | targets: [ 30 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 31 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 32 | .target( 33 | name: "DeviceKit", 34 | dependencies: [], 35 | path: "Source", 36 | resources: [.process("PrivacyInfo.xcprivacy")] 37 | ), 38 | .testTarget( 39 | name: "DeviceKitTests", 40 | dependencies: ["DeviceKit"], 41 | path: "Tests", 42 | resources: [.process("../Source/PrivacyInfo.xcprivacy")] 43 | ) 44 | ], 45 | swiftLanguageVersions: [.v5] 46 | ) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/devicekit/DeviceKit/master/LICENSE) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/DeviceKit.svg)](https://cocoapods.org/pods/DeviceKit) 5 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![codecov](https://codecov.io/gh/devicekit/DeviceKit/branch/master/graph/badge.svg)](https://codecov.io/gh/devicekit/DeviceKit) 7 | [![CocoaPods](https://img.shields.io/cocoapods/dt/DeviceKit.svg)](https://cocoapods.org/pods/DeviceKit) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/844e23a17bde71ff6be1/maintainability)](https://codeclimate.com/github/devicekit/DeviceKit/maintainability) 9 | [![Platform](https://img.shields.io/cocoapods/p/DeviceKit.svg?style=flat)](http://cocoadocs.org/docsets/DeviceKit) 10 | 11 | 12 | | Branch | Versions | 13 | |:---------|:----------:| 14 | | **master** | ≥ 2.0 | 15 | | **Swift 4 - 4.2** | ≥ 1.3 < 1.13 | 16 | | **Swift 3** | ≥ 1.0 < 1.3 | 17 | | **Swift 2.3** | < 1.0 | 18 | 19 | `DeviceKit` is a value-type replacement of [`UIDevice`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDevice_Class/). 20 | 21 | ## Current version 5.6.0 22 | See our detailed [changelog](CHANGELOG.md) for the latest features, improvements and bug fixes. 23 | 24 | ## Features 25 | 26 | - [x] Equatable 27 | - [x] Device identification 28 | - [x] Device family detection 29 | - [x] Device group detection 30 | - [x] Simulator detection 31 | - [x] Battery state 32 | - [x] Battery level 33 | - [x] Various device metrics (e.g. screen size, screen ratio, PPI) 34 | - [x] Low Power Mode detection 35 | - [x] Guided Access Session detection 36 | - [x] Screen brightness 37 | - [x] Display Zoom detection 38 | - [x] Detect available sensors (Touch ID, Face ID) 39 | - [x] Detect available disk space 40 | - [x] Apple Pencil support detection 41 | 42 | ## Requirements 43 | 44 | - iOS 11.0+ 45 | - tvOS 11.0+ 46 | - watchOS 4.0+ 47 | 48 | ## Installation 49 | DeviceKit can be installed in various ways. 50 | 51 | ### CocoaPods 52 | 53 | #### Swift 5 54 | ```ruby 55 | pod 'DeviceKit', '~> 5.2' 56 | ``` 57 | #### iOS 8.0 support 58 | ```ruby 59 | pod 'DeviceKit', '3.2' 60 | ``` 61 | #### Swift 4.0 - Swift 4.2 62 | ```ruby 63 | pod 'DeviceKit', '~> 1.3' 64 | ``` 65 | #### Swift 3 66 | ```ruby 67 | pod 'DeviceKit', '~> 1.2.3' 68 | ``` 69 | #### Swift 2.3 (Unsupported) 70 | ```ruby 71 | pod 'DeviceKit', :git => 'https://github.com/devicekit/DeviceKit.git', :branch => 'swift-2.3-unsupported' 72 | ``` 73 | 74 | ### Swift Package Manager 75 | 76 | #### Swift 5 77 | ```swift 78 | dependencies: [ 79 | .package(url: "https://github.com/devicekit/DeviceKit.git", from: "4.0.0"), 80 | /// ... 81 | ] 82 | ``` 83 | #### iOS 8.0 support 84 | ```swift 85 | dependencies: [ 86 | .package(url: "https://github.com/devicekit/DeviceKit.git", from: "3.2.0"), 87 | /// ... 88 | ] 89 | ``` 90 | 91 | ### Carthage 92 | 93 | #### Swift 5 94 | ```ogdl 95 | github "devicekit/DeviceKit" ~> 4.0 96 | ``` 97 | #### iOS 8.0 support 98 | ```ogdl 99 | github "devicekit/DeviceKit" ~> 3.2 100 | ``` 101 | #### Swift 4.0 - Swift 4.2 102 | ```ogdl 103 | github "devicekit/DeviceKit" ~> 1.3 104 | ``` 105 | #### Swift 3 106 | ```ogdl 107 | github "devicekit/DeviceKit" ~> 1.2.3 108 | ``` 109 | #### Swift 2.3 (Unsupported) 110 | ```ogdl 111 | github "devicekit/DeviceKit" "swift-2.3-unsupported" 112 | ``` 113 | 114 | ### Manually 115 | To install it manually, drag the `DeviceKit` project into your app project in Xcode. Or add it as a git submodule by running: 116 | ```bash 117 | $ git submodule add https://github.com/devicekit/DeviceKit.git 118 | ``` 119 | 120 | ## Usage 121 | First make sure to import the framework: 122 | ```swift 123 | import DeviceKit 124 | ``` 125 | 126 | Here are some usage examples. All devices are also available as simulators: 127 | ```swift 128 | .iPhone6 => .simulator(.iPhone6) 129 | .iPhone6s => .simulator(.iPhone6s) 130 | ``` 131 | 132 | You can try these examples in Playground. 133 | 134 | **Note:** 135 | 136 | > To try DeviceKit in the playground, open the `DeviceKit.xcworkspace` and build DeviceKit.framework for any simulator first by selecting "DeviceKit" as your current scheme. 137 | 138 | ### Get the Device You're Running On 139 | ```swift 140 | let device = Device.current 141 | 142 | print(device) // prints, for example, "iPhone 6 Plus" 143 | 144 | if device == .iPhone6Plus { 145 | // Do something 146 | } else { 147 | // Do something else 148 | } 149 | ``` 150 | 151 | ### Get the Device Family 152 | ```swift 153 | let device = Device.current 154 | if device.isPod { 155 | // iPods (real or simulator) 156 | } else if device.isPhone { 157 | // iPhone (real or simulator) 158 | } else if device.isPad { 159 | // iPad (real or simulator) 160 | } 161 | ``` 162 | 163 | ### Check If Running on Simulator 164 | ```swift 165 | let device = Device.current 166 | if device.isSimulator { 167 | // Running on one of the simulators(iPod/iPhone/iPad) 168 | // Skip doing something irrelevant for Simulator 169 | } 170 | ``` 171 | 172 | ### Get the Simulator Device 173 | ```swift 174 | let device = Device.current 175 | switch device { 176 | case .simulator(.iPhone6s): break // You're running on the iPhone 6s simulator 177 | case .simulator(.iPadAir2): break // You're running on the iPad Air 2 simulator 178 | default: break 179 | } 180 | ``` 181 | 182 | ### Make Sure the Device Is Contained in a Preconfigured Group 183 | ```swift 184 | let groupOfAllowedDevices: [Device] = [.iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .simulator(.iPhone6), .simulator(.iPhone6Plus),.simulator(.iPhone6s),.simulator(.iPhone6sPlus).simulator(.iPhone8),.simulator(.iPhone8Plus),.simulator(.iPhoneX),.simulator(.iPhoneXS),.simulator(.iPhoneXSMax),.simulator(.iPhoneXR)] 185 | 186 | let device = Device.current 187 | 188 | if device.isOneOf(groupOfAllowedDevices) { 189 | // Do your action 190 | } 191 | ``` 192 | 193 | ### Get the Current Battery State 194 | **Note:** 195 | 196 | > To get the current battery state we need to set `UIDevice.current.isBatteryMonitoringEnabled` to `true`. To avoid any issues with your code, we read the current setting and reset it to what it was before when we're done. 197 | 198 | ```swift 199 | if device.batteryState == .full || device.batteryState >= .charging(75) { 200 | print("Your battery is happy! 😊") 201 | } 202 | ``` 203 | 204 | ### Get the Current Battery Level 205 | ```swift 206 | if device.batteryLevel >= 50 { 207 | install_iOS() 208 | } else { 209 | showError() 210 | } 211 | ``` 212 | 213 | ### Get Low Power mode status 214 | ```swift 215 | if device.batteryState.lowPowerMode { 216 | print("Low Power mode is enabled! 🔋") 217 | } else { 218 | print("Low Power mode is disabled! 😊") 219 | } 220 | ``` 221 | 222 | ### Check if a Guided Access session is currently active 223 | ```swift 224 | if device.isGuidedAccessSessionActive { 225 | print("Guided Access session is currently active") 226 | } else { 227 | print("No Guided Access session is currently active") 228 | } 229 | ``` 230 | 231 | ### Get Screen Brightness 232 | ```swift 233 | if device.screenBrightness > 50 { 234 | print("Take care of your eyes!") 235 | } 236 | ``` 237 | 238 | ### Get Available Disk Space 239 | ```swift 240 | if Device.volumeAvailableCapacityForOpportunisticUsage ?? 0 > Int64(1_000_000) { 241 | // download that nice-to-have huge file 242 | } 243 | 244 | if Device.volumeAvailableCapacityForImportantUsage ?? 0 > Int64(1_000) { 245 | // download that file you really need 246 | } 247 | ``` 248 | 249 | ## Source of Information 250 | All model identifiers are taken from the following website: https://www.theiphonewiki.com/wiki/Models or extracted from the simulator app bundled with Xcode. 251 | 252 | ## Contributing 253 | If you have the need for a specific feature that you want implemented or if you experienced a bug, please open an issue. 254 | If you extended the functionality of DeviceKit yourself and want others to use it too, please submit a pull request. 255 | 256 | ## Contributors 257 | The complete list of people who contributed to this project is available [here](https://github.com/devicekit/DeviceKit/graphs/contributors). DeviceKit wouldn't be what it is without you! Thank you very much! 🙏 258 | -------------------------------------------------------------------------------- /Scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ~/.rvm/scripts/rvm 4 | rvm use default 5 | pod trunk push 6 | -------------------------------------------------------------------------------- /Source/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 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Source/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTrackingDomains 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyAccessedAPITypes 10 | 11 | 12 | NSPrivacyAccessedAPITypeReasons 13 | 14 | 85F4.1 15 | E174.1 16 | 17 | NSPrivacyAccessedAPIType 18 | NSPrivacyAccessedAPICategoryDiskSpace 19 | 20 | 21 | NSPrivacyTracking 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the DeviceKit open source project 4 | // 5 | // Copyright © 2014 - 2018 Dennis Weissmann and the DeviceKit project authors 6 | // 7 | // License: https://github.com/dennisweissmann/DeviceKit/blob/master/LICENSE 8 | // Contributors: https://github.com/dennisweissmann/DeviceKit#contributors 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @testable import DeviceKit 13 | import XCTest 14 | 15 | class DeviceKitTests: XCTestCase { 16 | 17 | let device = Device.current 18 | 19 | func testDeviceSimulator() { 20 | #if os(macOS) 21 | XCTAssertFalse(device.isOneOf(Device.allSimulators)) 22 | #else 23 | XCTAssertTrue(device.isOneOf(Device.allSimulators)) 24 | #endif 25 | } 26 | 27 | func testIsSimulator() { 28 | #if os(macOS) 29 | XCTAssertFalse(device.isSimulator) 30 | #else 31 | XCTAssertTrue(device.isSimulator) 32 | #endif 33 | } 34 | 35 | func testDeviceDescription() { 36 | #if os(macOS) 37 | #else 38 | XCTAssertTrue(device.description.hasPrefix("Simulator")) 39 | XCTAssertTrue(device.description.contains("iPhone") 40 | || device.description.contains("iPad") 41 | || device.description.contains("iPod") 42 | || device.description.contains("TV") 43 | || device.description.contains("Apple Watch")) 44 | #endif 45 | } 46 | 47 | func testIsCanvas() { 48 | #if os(iOS) 49 | let otherDevice: Device = device == .iPhone14Pro ? .iPhone14ProMax : .iPhone14Pro 50 | #elseif os(tvOS) 51 | let otherDevice: Device = device == .appleTVHD ? .appleTV4K : .appleTVHD 52 | #elseif os(watchOS) 53 | let otherDevice: Device = device == .appleWatchUltra ? .appleWatchSeries8_41mm : .appleWatchUltra 54 | #else 55 | let otherDevice: Device = .unknown("mac") 56 | #endif 57 | XCTAssertEqual(otherDevice.isCanvas, nil) 58 | XCTAssertEqual(device.isCanvas, false) 59 | } 60 | 61 | #if os(iOS) || os(tvOS) 62 | func testDeviceCPU() { 63 | #if os(iOS) 64 | XCTAssertEqual(Device.iPhone12Mini.cpu, Device.CPU.a14Bionic) 65 | XCTAssertEqual(Device.iPhone12.cpu, Device.CPU.a14Bionic) 66 | XCTAssertEqual(Device.iPhone12Pro.cpu, Device.CPU.a14Bionic) 67 | XCTAssertEqual(Device.iPhone12ProMax.cpu, Device.CPU.a14Bionic) 68 | XCTAssertEqual(Device.iPhone13Mini.cpu, Device.CPU.a15Bionic) 69 | XCTAssertEqual(Device.iPhone13.cpu, Device.CPU.a15Bionic) 70 | XCTAssertEqual(Device.iPhone13Pro.cpu, Device.CPU.a15Bionic) 71 | XCTAssertEqual(Device.iPhone13ProMax.cpu, Device.CPU.a15Bionic) 72 | XCTAssertEqual(Device.iPhone14.cpu, Device.CPU.a15Bionic) 73 | XCTAssertEqual(Device.iPhone14Plus.cpu, Device.CPU.a15Bionic) 74 | XCTAssertEqual(Device.iPhone14Pro.cpu, Device.CPU.a16Bionic) 75 | XCTAssertEqual(Device.iPhone14ProMax.cpu, Device.CPU.a16Bionic) 76 | 77 | XCTAssertEqual(Device.iPad8.cpu, Device.CPU.a12Bionic) 78 | XCTAssertEqual(Device.iPad9.cpu, Device.CPU.a13Bionic) 79 | XCTAssertEqual(Device.iPad10.cpu, Device.CPU.a14Bionic) 80 | XCTAssertEqual(Device.iPadAir3.cpu, Device.CPU.a12Bionic) 81 | XCTAssertEqual(Device.iPadAir4.cpu, Device.CPU.a14Bionic) 82 | XCTAssertEqual(Device.iPadAir5.cpu, Device.CPU.m1) 83 | XCTAssertEqual(Device.iPadMini4.cpu, Device.CPU.a8) 84 | XCTAssertEqual(Device.iPadMini5.cpu, Device.CPU.a12Bionic) 85 | XCTAssertEqual(Device.iPadMini6.cpu, Device.CPU.a15Bionic) 86 | XCTAssertEqual(Device.iPadMiniA17Pro.cpu, Device.CPU.a17Pro) 87 | XCTAssertEqual(Device.iPadPro11Inch.cpu, Device.CPU.a12XBionic) 88 | XCTAssertEqual(Device.iPadPro11Inch2.cpu, Device.CPU.a12ZBionic) 89 | XCTAssertEqual(Device.iPadPro11Inch3.cpu, Device.CPU.m1) 90 | XCTAssertEqual(Device.iPadPro11Inch4.cpu, Device.CPU.m2) 91 | #elseif os(tvOS) 92 | XCTAssertEqual(Device.appleTVHD.cpu, Device.CPU.a8) 93 | XCTAssertEqual(Device.appleTV4K.cpu, Device.CPU.a10XFusion) 94 | XCTAssertEqual(Device.appleTV4K2.cpu, Device.CPU.a12Bionic) 95 | XCTAssertEqual(Device.appleTV4K3.cpu, Device.CPU.a15Bionic) 96 | #endif 97 | } 98 | 99 | func testCPUDescription() { 100 | XCTAssertEqual(Device.CPU.a15Bionic.description, "A15 Bionic") 101 | XCTAssertEqual(Device.CPU.a16Bionic.description, "A16 Bionic") 102 | XCTAssertEqual(Device.CPU.m1.description, "M1") 103 | XCTAssertEqual(Device.CPU.m2.description, "M2") 104 | } 105 | #endif 106 | 107 | // MARK: - iOS 108 | #if os(iOS) 109 | 110 | func testIsPhoneIsPadIsPod() { 111 | // Test for https://github.com/devicekit/DeviceKit/issues/165 to prevent it from happening in the future. 112 | 113 | if UIDevice.current.userInterfaceIdiom == .pad { 114 | XCTAssertTrue(device.isPad) 115 | XCTAssertFalse(device.isPhone) 116 | XCTAssertFalse(device.isPod) 117 | } else if UIDevice.current.userInterfaceIdiom == .phone { 118 | XCTAssertFalse(device.isPad) 119 | if device.description.contains("iPod") { 120 | XCTAssertFalse(device.isPhone) 121 | XCTAssertTrue(device.isPod) 122 | } else { 123 | XCTAssertTrue(device.isPhone) 124 | XCTAssertFalse(device.isPod) 125 | } 126 | } 127 | 128 | for pad in Device.allPads { 129 | XCTAssertTrue(pad.isPad) 130 | XCTAssertFalse(pad.isPhone) 131 | XCTAssertFalse(pad.isPod) 132 | } 133 | for phone in Device.allPhones { 134 | XCTAssertFalse(phone.isPad) 135 | XCTAssertTrue(phone.isPhone) 136 | XCTAssertFalse(phone.isPod) 137 | } 138 | for pod in Device.allPods { 139 | XCTAssertFalse(pod.isPad) 140 | XCTAssertFalse(pod.isPhone) 141 | XCTAssertTrue(pod.isPod) 142 | } 143 | } 144 | 145 | func testSystemName() { 146 | if UIDevice.current.userInterfaceIdiom == .pad { 147 | XCTAssertEqual(device.systemName, "iPadOS") 148 | } else if UIDevice.current.userInterfaceIdiom == .phone { 149 | XCTAssertEqual(device.systemName, "iOS") 150 | } 151 | } 152 | 153 | func testBattery() { 154 | XCTAssertTrue(Device.BatteryState.full > Device.BatteryState.charging(100)) 155 | XCTAssertTrue(Device.BatteryState.charging(75) != Device.BatteryState.unplugged(75)) 156 | XCTAssertTrue(Device.BatteryState.unplugged(2) > Device.BatteryState.charging(1)) 157 | } 158 | 159 | func testMapFromIdentifier() { // swiftlint:disable:this function_body_length 160 | XCTAssertEqual(Device.mapToDevice(identifier: "iPod5,1"), .iPodTouch5) 161 | XCTAssertEqual(Device.mapToDevice(identifier: "iPod7,1"), .iPodTouch6) 162 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone3,1"), .iPhone4) 163 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone3,2"), .iPhone4) 164 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone3,3"), .iPhone4) 165 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone4,1"), .iPhone4s) 166 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone5,1"), .iPhone5) 167 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone5,2"), .iPhone5) 168 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone5,3"), .iPhone5c) 169 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone5,4"), .iPhone5c) 170 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone6,1"), .iPhone5s) 171 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone6,2"), .iPhone5s) 172 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone7,2"), .iPhone6) 173 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone7,1"), .iPhone6Plus) 174 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone8,1"), .iPhone6s) 175 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone8,2"), .iPhone6sPlus) 176 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone9,1"), .iPhone7) 177 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone9,3"), .iPhone7) 178 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone9,2"), .iPhone7Plus) 179 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone9,4"), .iPhone7Plus) 180 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone8,4"), .iPhoneSE) 181 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,1"), .iPhone8) 182 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,4"), .iPhone8) 183 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,2"), .iPhone8Plus) 184 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,5"), .iPhone8Plus) 185 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,3"), .iPhoneX) 186 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone10,6"), .iPhoneX) 187 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone11,2"), .iPhoneXS) 188 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone11,4"), .iPhoneXSMax) 189 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone11,6"), .iPhoneXSMax) 190 | XCTAssertEqual(Device.mapToDevice(identifier: "iPhone11,8"), .iPhoneXR) 191 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,1"), .iPad2) 192 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,2"), .iPad2) 193 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,3"), .iPad2) 194 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,4"), .iPad2) 195 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,1"), .iPad3) 196 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,2"), .iPad3) 197 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,3"), .iPad3) 198 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,4"), .iPad4) 199 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,5"), .iPad4) 200 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad3,6"), .iPad4) 201 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,1"), .iPadAir) 202 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,2"), .iPadAir) 203 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,3"), .iPadAir) 204 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad5,3"), .iPadAir2) 205 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad5,4"), .iPadAir2) 206 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,11"), .iPad5) 207 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,12"), .iPad5) 208 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,5"), .iPadMini) 209 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,6"), .iPadMini) 210 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad2,7"), .iPadMini) 211 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,4"), .iPadMini2) 212 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,5"), .iPadMini2) 213 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,6"), .iPadMini2) 214 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,7"), .iPadMini3) 215 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,8"), .iPadMini3) 216 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad4,9"), .iPadMini3) 217 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad5,1"), .iPadMini4) 218 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad5,2"), .iPadMini4) 219 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,3"), .iPadPro9Inch) 220 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,4"), .iPadPro9Inch) 221 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,7"), .iPadPro12Inch) 222 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad6,8"), .iPadPro12Inch) 223 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad7,1"), .iPadPro12Inch2) 224 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad7,2"), .iPadPro12Inch2) 225 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad7,3"), .iPadPro10Inch) 226 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad7,4"), .iPadPro10Inch) 227 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,1"), .iPadPro11Inch) 228 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,2"), .iPadPro11Inch) 229 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,3"), .iPadPro11Inch) 230 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,4"), .iPadPro11Inch) 231 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,5"), .iPadPro12Inch3) 232 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,6"), .iPadPro12Inch3) 233 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,7"), .iPadPro12Inch3) 234 | XCTAssertEqual(Device.mapToDevice(identifier: "iPad8,8"), .iPadPro12Inch3) 235 | } 236 | 237 | func testScreenRatio() { 238 | XCTAssertTrue(Device.iPodTouch5.screenRatio == (width: 9, height: 16)) 239 | XCTAssertTrue(Device.iPodTouch6.screenRatio == (width: 9, height: 16)) 240 | XCTAssertTrue(Device.iPhone4.screenRatio == (width: 2, height: 3)) 241 | XCTAssertTrue(Device.iPhone4s.screenRatio == (width: 2, height: 3)) 242 | XCTAssertTrue(Device.iPhone5.screenRatio == (width: 9, height: 16)) 243 | XCTAssertTrue(Device.iPhone5c.screenRatio == (width: 9, height: 16)) 244 | XCTAssertTrue(Device.iPhone5s.screenRatio == (width: 9, height: 16)) 245 | XCTAssertTrue(Device.iPhone6.screenRatio == (width: 9, height: 16)) 246 | XCTAssertTrue(Device.iPhone6Plus.screenRatio == (width: 9, height: 16)) 247 | XCTAssertTrue(Device.iPhone6s.screenRatio == (width: 9, height: 16)) 248 | XCTAssertTrue(Device.iPhone6sPlus.screenRatio == (width: 9, height: 16)) 249 | XCTAssertTrue(Device.iPhone7.screenRatio == (width: 9, height: 16)) 250 | XCTAssertTrue(Device.iPhone7Plus.screenRatio == (width: 9, height: 16)) 251 | XCTAssertTrue(Device.iPhoneSE.screenRatio == (width: 9, height: 16)) 252 | XCTAssertTrue(Device.iPhoneSE2.screenRatio == (width: 9, height: 16)) 253 | XCTAssertTrue(Device.iPhone8.screenRatio == (width: 9, height: 16)) 254 | XCTAssertTrue(Device.iPhone8Plus.screenRatio == (width: 9, height: 16)) 255 | XCTAssertTrue(Device.iPhoneX.screenRatio == (width: 9, height: 19.5)) 256 | XCTAssertTrue(Device.iPhoneXS.screenRatio == (width: 9, height: 19.5)) 257 | XCTAssertTrue(Device.iPhoneXSMax.screenRatio == (width: 9, height: 19.5)) 258 | XCTAssertTrue(Device.iPhoneXR.screenRatio == (width: 9, height: 19.5)) 259 | XCTAssertTrue(Device.iPad2.screenRatio == (width: 3, height: 4)) 260 | XCTAssertTrue(Device.iPad3.screenRatio == (width: 3, height: 4)) 261 | XCTAssertTrue(Device.iPad4.screenRatio == (width: 3, height: 4)) 262 | XCTAssertTrue(Device.iPadAir.screenRatio == (width: 3, height: 4)) 263 | XCTAssertTrue(Device.iPadAir2.screenRatio == (width: 3, height: 4)) 264 | XCTAssertTrue(Device.iPad5.screenRatio == (width: 3, height: 4)) 265 | XCTAssertTrue(Device.iPadMini.screenRatio == (width: 3, height: 4)) 266 | XCTAssertTrue(Device.iPadMini2.screenRatio == (width: 3, height: 4)) 267 | XCTAssertTrue(Device.iPadMini3.screenRatio == (width: 3, height: 4)) 268 | XCTAssertTrue(Device.iPadMini4.screenRatio == (width: 3, height: 4)) 269 | XCTAssertTrue(Device.iPadPro9Inch.screenRatio == (width: 3, height: 4)) 270 | XCTAssertTrue(Device.iPadPro12Inch.screenRatio == (width: 3, height: 4)) 271 | XCTAssertTrue(Device.iPadPro12Inch2.screenRatio == (width: 3, height: 4)) 272 | XCTAssertTrue(Device.iPadPro10Inch.screenRatio == (width: 3, height: 4)) 273 | XCTAssertTrue(Device.iPadPro11Inch.screenRatio == (width: 139, height: 199)) 274 | XCTAssertTrue(Device.iPadPro12Inch3.screenRatio == (width: 512, height: 683)) 275 | 276 | XCTAssertTrue(Device.simulator(device).screenRatio == device.screenRatio) 277 | XCTAssertTrue(Device.unknown(UUID().uuidString).screenRatio == (width: -1, height: -1)) 278 | } 279 | 280 | func testDiagonal() { 281 | XCTAssertEqual(Device.iPhone4.diagonal, 3.5) 282 | XCTAssertEqual(Device.iPhone4s.diagonal, 3.5) 283 | 284 | XCTAssertEqual(Device.iPodTouch5.diagonal, 4) 285 | XCTAssertEqual(Device.iPodTouch6.diagonal, 4) 286 | XCTAssertEqual(Device.iPhone5.diagonal, 4) 287 | XCTAssertEqual(Device.iPhone5c.diagonal, 4) 288 | XCTAssertEqual(Device.iPhone5s.diagonal, 4) 289 | XCTAssertEqual(Device.iPhoneSE.diagonal, 4) 290 | 291 | XCTAssertEqual(Device.iPhone6.diagonal, 4.7) 292 | XCTAssertEqual(Device.iPhone6s.diagonal, 4.7) 293 | XCTAssertEqual(Device.iPhone7.diagonal, 4.7) 294 | XCTAssertEqual(Device.iPhone8.diagonal, 4.7) 295 | 296 | XCTAssertEqual(Device.iPhone6Plus.diagonal, 5.5) 297 | XCTAssertEqual(Device.iPhone6sPlus.diagonal, 5.5) 298 | XCTAssertEqual(Device.iPhone7Plus.diagonal, 5.5) 299 | XCTAssertEqual(Device.iPhone8Plus.diagonal, 5.5) 300 | XCTAssertEqual(Device.iPhoneX.diagonal, 5.8) 301 | XCTAssertEqual(Device.iPhoneXS.diagonal, 5.8) 302 | XCTAssertEqual(Device.iPhoneXSMax.diagonal, 6.5) 303 | XCTAssertEqual(Device.iPhoneXR.diagonal, 6.1) 304 | 305 | XCTAssertEqual(Device.iPad2.diagonal, 9.7) 306 | XCTAssertEqual(Device.iPad3.diagonal, 9.7) 307 | XCTAssertEqual(Device.iPad4.diagonal, 9.7) 308 | XCTAssertEqual(Device.iPadAir.diagonal, 9.7) 309 | XCTAssertEqual(Device.iPadAir2.diagonal, 9.7) 310 | XCTAssertEqual(Device.iPad5.diagonal, 9.7) 311 | 312 | XCTAssertEqual(Device.iPadMini.diagonal, 7.9) 313 | XCTAssertEqual(Device.iPadMini2.diagonal, 7.9) 314 | XCTAssertEqual(Device.iPadMini3.diagonal, 7.9) 315 | XCTAssertEqual(Device.iPadMini4.diagonal, 7.9) 316 | 317 | XCTAssertEqual(Device.iPadPro9Inch.diagonal, 9.7) 318 | XCTAssertEqual(Device.iPadPro12Inch.diagonal, 12.9) 319 | XCTAssertEqual(Device.iPadPro12Inch2.diagonal, 12.9) 320 | XCTAssertEqual(Device.iPadPro10Inch.diagonal, 10.5) 321 | XCTAssertEqual(Device.iPadPro11Inch.diagonal, 11.0) 322 | XCTAssertEqual(Device.iPadPro12Inch3.diagonal, 12.9) 323 | 324 | XCTAssertEqual(Device.simulator(.iPadPro10Inch).diagonal, 10.5) 325 | XCTAssertEqual(Device.unknown(UUID().uuidString).diagonal, -1) 326 | } 327 | 328 | func testDescription() { // swiftlint:disable:this function_body_length 329 | XCTAssertEqual(Device.iPodTouch5.description, "iPod touch (5th generation)") 330 | XCTAssertEqual(Device.iPodTouch6.description, "iPod touch (6th generation)") 331 | XCTAssertEqual(Device.iPodTouch7.description, "iPod touch (7th generation)") 332 | XCTAssertEqual(Device.iPhone4.description, "iPhone 4") 333 | XCTAssertEqual(Device.iPhone4s.description, "iPhone 4s") 334 | XCTAssertEqual(Device.iPhone5.description, "iPhone 5") 335 | XCTAssertEqual(Device.iPhone5c.description, "iPhone 5c") 336 | XCTAssertEqual(Device.iPhone5s.description, "iPhone 5s") 337 | XCTAssertEqual(Device.iPhone6.description, "iPhone 6") 338 | XCTAssertEqual(Device.iPhone6Plus.description, "iPhone 6 Plus") 339 | XCTAssertEqual(Device.iPhone6s.description, "iPhone 6s") 340 | XCTAssertEqual(Device.iPhone6sPlus.description, "iPhone 6s Plus") 341 | XCTAssertEqual(Device.iPhone7.description, "iPhone 7") 342 | XCTAssertEqual(Device.iPhone7Plus.description, "iPhone 7 Plus") 343 | XCTAssertEqual(Device.iPhoneSE.description, "iPhone SE") 344 | XCTAssertEqual(Device.iPhone8.description, "iPhone 8") 345 | XCTAssertEqual(Device.iPhone8Plus.description, "iPhone 8 Plus") 346 | XCTAssertEqual(Device.iPhoneX.description, "iPhone X") 347 | XCTAssertEqual(Device.iPhoneXS.description, "iPhone Xs") 348 | XCTAssertEqual(Device.iPhoneXSMax.description, "iPhone Xs Max") 349 | XCTAssertEqual(Device.iPhoneXR.description, "iPhone Xʀ") 350 | XCTAssertEqual(Device.iPad2.description, "iPad 2") 351 | XCTAssertEqual(Device.iPad3.description, "iPad (3rd generation)") 352 | XCTAssertEqual(Device.iPad4.description, "iPad (4th generation)") 353 | XCTAssertEqual(Device.iPadAir.description, "iPad Air") 354 | XCTAssertEqual(Device.iPadAir2.description, "iPad Air 2") 355 | XCTAssertEqual(Device.iPad5.description, "iPad (5th generation)") 356 | XCTAssertEqual(Device.iPad6.description, "iPad (6th generation)") 357 | XCTAssertEqual(Device.iPadAir3.description, "iPad Air (3rd generation)") 358 | XCTAssertEqual(Device.iPadMini.description, "iPad Mini") 359 | XCTAssertEqual(Device.iPadMini2.description, "iPad Mini 2") 360 | XCTAssertEqual(Device.iPadMini3.description, "iPad Mini 3") 361 | XCTAssertEqual(Device.iPadMini4.description, "iPad Mini 4") 362 | XCTAssertEqual(Device.iPadMini5.description, "iPad Mini (5th generation)") 363 | XCTAssertEqual(Device.iPadPro9Inch.description, "iPad Pro (9.7-inch)") 364 | XCTAssertEqual(Device.iPadPro12Inch.description, "iPad Pro (12.9-inch)") 365 | XCTAssertEqual(Device.iPadPro12Inch2.description, "iPad Pro (12.9-inch) (2nd generation)") 366 | XCTAssertEqual(Device.iPadPro10Inch.description, "iPad Pro (10.5-inch)") 367 | XCTAssertEqual(Device.iPadPro11Inch.description, "iPad Pro (11-inch)") 368 | XCTAssertEqual(Device.iPadPro12Inch3.description, "iPad Pro (12.9-inch) (3rd generation)") 369 | XCTAssertEqual(Device.simulator(Device.iPadPro10Inch).description, "Simulator (\(Device.iPadPro10Inch))") 370 | let uuid = UUID().uuidString 371 | XCTAssertEqual(Device.unknown(uuid).description, uuid) 372 | } 373 | 374 | func testSafeDescription() { 375 | for device in Device.allRealDevices { 376 | switch device { 377 | case .iPhoneXR, .iPhoneXS, .iPhoneXSMax: 378 | XCTAssertNotEqual(device.description, device.safeDescription) 379 | default: 380 | XCTAssertEqual(device.description, device.safeDescription) 381 | } 382 | } 383 | } 384 | 385 | func testIsPad() { 386 | Device.allPads.forEach { XCTAssertTrue($0.isPad) } 387 | } 388 | 389 | // Test that all the ppi values for applicable devices match the public information available at wikipedia. 390 | // Non-applicable devices return nil. 391 | func testPPI() { 392 | // swiftlint:disable comma 393 | assertEqualDeviceAndSimulator(device: Device.iPodTouch5, property: \Device.ppi, value: 326) 394 | assertEqualDeviceAndSimulator(device: Device.iPodTouch6, property: \Device.ppi, value: 326) 395 | 396 | assertEqualDeviceAndSimulator(device: Device.iPhone4, property: \Device.ppi, value: 326) 397 | assertEqualDeviceAndSimulator(device: Device.iPhone4s, property: \Device.ppi, value: 326) 398 | assertEqualDeviceAndSimulator(device: Device.iPhone5, property: \Device.ppi, value: 326) 399 | assertEqualDeviceAndSimulator(device: Device.iPhone5c, property: \Device.ppi, value: 326) 400 | assertEqualDeviceAndSimulator(device: Device.iPhone5s, property: \Device.ppi, value: 326) 401 | assertEqualDeviceAndSimulator(device: Device.iPhone6, property: \Device.ppi, value: 326) 402 | assertEqualDeviceAndSimulator(device: Device.iPhone6Plus, property: \Device.ppi, value: 401) 403 | assertEqualDeviceAndSimulator(device: Device.iPhone6s, property: \Device.ppi, value: 326) 404 | assertEqualDeviceAndSimulator(device: Device.iPhone6sPlus, property: \Device.ppi, value: 401) 405 | assertEqualDeviceAndSimulator(device: Device.iPhone7, property: \Device.ppi, value: 326) 406 | assertEqualDeviceAndSimulator(device: Device.iPhone7Plus, property: \Device.ppi, value: 401) 407 | assertEqualDeviceAndSimulator(device: Device.iPhoneSE, property: \Device.ppi, value: 326) 408 | assertEqualDeviceAndSimulator(device: Device.iPhoneSE2, property: \Device.ppi, value: 326) 409 | assertEqualDeviceAndSimulator(device: Device.iPhone8, property: \Device.ppi, value: 326) 410 | assertEqualDeviceAndSimulator(device: Device.iPhone8Plus, property: \Device.ppi, value: 401) 411 | assertEqualDeviceAndSimulator(device: Device.iPhoneX, property: \Device.ppi, value: 458) 412 | assertEqualDeviceAndSimulator(device: Device.iPhoneXR, property: \Device.ppi, value: 326) 413 | assertEqualDeviceAndSimulator(device: Device.iPhoneXS, property: \Device.ppi, value: 458) 414 | assertEqualDeviceAndSimulator(device: Device.iPhoneXSMax, property: \Device.ppi, value: 458) 415 | 416 | assertEqualDeviceAndSimulator(device: Device.iPad2, property: \Device.ppi, value: 132) 417 | assertEqualDeviceAndSimulator(device: Device.iPad3, property: \Device.ppi, value: 264) 418 | assertEqualDeviceAndSimulator(device: Device.iPad4, property: \Device.ppi, value: 264) 419 | assertEqualDeviceAndSimulator(device: Device.iPadAir, property: \Device.ppi, value: 264) 420 | assertEqualDeviceAndSimulator(device: Device.iPadAir2, property: \Device.ppi, value: 264) 421 | assertEqualDeviceAndSimulator(device: Device.iPad5, property: \Device.ppi, value: 264) 422 | assertEqualDeviceAndSimulator(device: Device.iPad6, property: \Device.ppi, value: 264) 423 | assertEqualDeviceAndSimulator(device: Device.iPadMini, property: \Device.ppi, value: 163) 424 | assertEqualDeviceAndSimulator(device: Device.iPadMini2, property: \Device.ppi, value: 326) 425 | assertEqualDeviceAndSimulator(device: Device.iPadMini3, property: \Device.ppi, value: 326) 426 | assertEqualDeviceAndSimulator(device: Device.iPadMini4, property: \Device.ppi, value: 326) 427 | assertEqualDeviceAndSimulator(device: Device.iPadPro9Inch, property: \Device.ppi, value: 264) 428 | assertEqualDeviceAndSimulator(device: Device.iPadPro12Inch, property: \Device.ppi, value: 264) 429 | assertEqualDeviceAndSimulator(device: Device.iPadPro12Inch2, property: \Device.ppi, value: 264) 430 | assertEqualDeviceAndSimulator(device: Device.iPadPro10Inch, property: \Device.ppi, value: 264) 431 | assertEqualDeviceAndSimulator(device: Device.iPadPro11Inch, property: \Device.ppi, value: 264) 432 | assertEqualDeviceAndSimulator(device: Device.iPadPro12Inch3, property: \Device.ppi, value: 264) 433 | // swiftlint:enable comma 434 | } 435 | 436 | func assertEqualDeviceAndSimulator(device: Device, 437 | property: KeyPath, 438 | value: Value, 439 | file: StaticString = #file, 440 | line: UInt = #line) where Value: Equatable { 441 | let simulator = Device.simulator(device) 442 | XCTAssertTrue(device[keyPath: property] == value, file: file, line: line) 443 | XCTAssertTrue(simulator[keyPath: property] == value, file: file, line: line) 444 | } 445 | 446 | func testIsPlusSized() { 447 | XCTAssertEqual(Device.allPlusSizedDevices, [ 448 | .iPhone6Plus, 449 | .iPhone6sPlus, 450 | .iPhone7Plus, 451 | .iPhone8Plus, 452 | .iPhoneXSMax, 453 | .iPhone11ProMax, 454 | .iPhone12ProMax, 455 | .iPhone13ProMax, 456 | .iPhone14Plus, 457 | .iPhone14ProMax, 458 | .iPhone15Plus, 459 | .iPhone15ProMax, 460 | .iPhone16Plus, 461 | .iPhone16ProMax, 462 | ]) 463 | } 464 | 465 | func testIsPro() { 466 | XCTAssertEqual(Device.allProDevices, [ 467 | .iPhone11Pro, 468 | .iPhone11ProMax, 469 | .iPhone12Pro, 470 | .iPhone12ProMax, 471 | .iPhone13Pro, 472 | .iPhone13ProMax, 473 | .iPhone14Pro, 474 | .iPhone14ProMax, 475 | .iPhone15Pro, 476 | .iPhone15ProMax, 477 | .iPhone16Pro, 478 | .iPhone16ProMax, 479 | .iPadPro9Inch, 480 | .iPadPro12Inch, 481 | .iPadPro12Inch2, 482 | .iPadPro10Inch, 483 | .iPadPro11Inch, 484 | .iPadPro12Inch3, 485 | .iPadPro11Inch2, 486 | .iPadPro12Inch4, 487 | .iPadPro11Inch3, 488 | .iPadPro12Inch5, 489 | .iPadPro11Inch4, 490 | .iPadPro12Inch6, 491 | .iPadPro11M4, 492 | .iPadPro13M4, 493 | ]) 494 | } 495 | 496 | func testGuidedAccessSession() { 497 | XCTAssertFalse(Device.current.isGuidedAccessSessionActive) 498 | } 499 | 500 | // enable once unit tests can be run on device 501 | func testKeepsBatteryMonitoringState() { 502 | UIDevice.current.isBatteryMonitoringEnabled = true 503 | XCTAssertTrue(UIDevice.current.isBatteryMonitoringEnabled) 504 | _ = Device.current.batteryState 505 | XCTAssertTrue(UIDevice.current.isBatteryMonitoringEnabled) 506 | 507 | UIDevice.current.isBatteryMonitoringEnabled = false 508 | _ = Device.current.batteryState 509 | XCTAssertFalse(UIDevice.current.isBatteryMonitoringEnabled) 510 | } 511 | 512 | func testHasDynamicIsland() { 513 | let dynamicIslandDevices: [Device] = [ 514 | .iPhone14Pro, 515 | .iPhone14ProMax, 516 | .iPhone15, 517 | .iPhone15Plus, 518 | .iPhone15Pro, 519 | .iPhone15ProMax, 520 | .iPhone16, 521 | .iPhone16Plus, 522 | .iPhone16Pro, 523 | .iPhone16ProMax, 524 | ] 525 | for device in Device.allRealDevices { 526 | XCTAssertTrue(device.hasDynamicIsland == device.isOneOf(dynamicIslandDevices), "testHasDynamicIsland failed for \(device.description)") 527 | } 528 | } 529 | 530 | func testHas5gSupport() { 531 | let has5gDevices: [Device] = [ 532 | .iPhone12, 533 | .iPhone12Mini, 534 | .iPhone12Pro, 535 | .iPhone12ProMax, 536 | .iPhone13, 537 | .iPhone13Mini, 538 | .iPhone13Pro, 539 | .iPhone13ProMax, 540 | .iPhoneSE3, 541 | .iPhone14, 542 | .iPhone14Plus, 543 | .iPhone14Pro, 544 | .iPhone14ProMax, 545 | .iPhone15, 546 | .iPhone15Plus, 547 | .iPhone15Pro, 548 | .iPhone15ProMax, 549 | .iPhone16, 550 | .iPhone16Plus, 551 | .iPhone16Pro, 552 | .iPhone16ProMax, 553 | .iPhone16e, 554 | .iPad10, 555 | .iPadA16, 556 | .iPadAir5, 557 | .iPadAir11M2, 558 | .iPadAir13M2, 559 | .iPadAir11M3, 560 | .iPadAir13M3, 561 | .iPadMini6, 562 | .iPadMiniA17Pro, 563 | .iPadPro11Inch3, 564 | .iPadPro12Inch5, 565 | .iPadPro11Inch4, 566 | .iPadPro12Inch6, 567 | .iPadPro11M4, 568 | .iPadPro13M4, 569 | ] 570 | for device in Device.allRealDevices { 571 | XCTAssertTrue(device.has5gSupport == device.isOneOf(has5gDevices), "testHas5gSupport failed for \(device.description)") 572 | } 573 | } 574 | 575 | // MARK: - volumes 576 | @available(iOS 11.0, *) 577 | func testVolumeTotalCapacity() { 578 | XCTAssertNotNil(Device.volumeTotalCapacity) 579 | } 580 | 581 | @available(iOS 11.0, *) 582 | func testVolumeAvailableCapacity() { 583 | XCTAssertNotNil(Device.volumeAvailableCapacity) 584 | } 585 | 586 | @available(iOS 11.0, *) 587 | func testVolumeAvailableCapacityForImportantUsage() { 588 | XCTAssertNotNil(Device.volumeAvailableCapacityForImportantUsage) 589 | } 590 | 591 | @available(iOS 11.0, *) 592 | func testVolumeAvailableCapacityForOpportunisticUsage() { 593 | XCTAssertNotNil(Device.volumeAvailableCapacityForOpportunisticUsage) 594 | } 595 | 596 | @available(iOS 11.0, *) 597 | func testVolumes() { 598 | XCTAssertNotNil(Device.volumes) 599 | } 600 | 601 | func testCameras() { 602 | for device in Device.allDevicesWithCamera { 603 | XCTAssertTrue(device.cameras.contains(.wide) || device.cameras.contains(.telephoto) || device.cameras.contains(.ultraWide)) 604 | XCTAssertTrue(device.hasCamera) 605 | XCTAssertTrue(device.hasWideCamera || device.hasTelephotoCamera || device.hasUltraWideCamera) 606 | } 607 | for device in Device.allPhones + Device.allPads + Device.allPods { 608 | if !Device.allDevicesWithCamera.contains(device) { 609 | XCTAssertFalse(device.cameras.contains(.wide)) 610 | XCTAssertFalse(device.cameras.contains(.telephoto)) 611 | XCTAssertFalse(device.cameras.contains(.ultraWide)) 612 | XCTAssertFalse(device.hasCamera) 613 | XCTAssertFalse(device.hasWideCamera) 614 | XCTAssertFalse(device.hasTelephotoCamera) 615 | XCTAssertFalse(device.hasUltraWideCamera) 616 | } 617 | } 618 | for device in Device.allDevicesWithWideCamera { 619 | XCTAssertTrue(device.cameras.contains(.wide)) 620 | XCTAssertTrue(device.hasCamera) 621 | XCTAssertTrue(device.hasWideCamera) 622 | } 623 | for device in Device.allDevicesWithTelephotoCamera { 624 | XCTAssertTrue(device.cameras.contains(.telephoto)) 625 | XCTAssertTrue(device.hasCamera) 626 | XCTAssertTrue(device.hasTelephotoCamera) 627 | } 628 | for device in Device.allDevicesWithUltraWideCamera { 629 | XCTAssertTrue(device.cameras.contains(.ultraWide)) 630 | XCTAssertTrue(device.hasCamera) 631 | XCTAssertTrue(device.hasUltraWideCamera) 632 | } 633 | } 634 | 635 | func testLidarValues() { 636 | let lidarDevices: [Device] = [ 637 | .iPhone12Pro, 638 | .iPhone12ProMax, 639 | .iPhone13Pro, 640 | .iPhone13ProMax, 641 | .iPhone14Pro, 642 | .iPhone14ProMax, 643 | .iPhone15Pro, 644 | .iPhone15ProMax, 645 | .iPhone16Pro, 646 | .iPhone16ProMax, 647 | .iPadPro11Inch2, 648 | .iPadPro12Inch4, 649 | .iPadPro11Inch3, 650 | .iPadPro12Inch5, 651 | .iPadPro11Inch4, 652 | .iPadPro12Inch6, 653 | .iPadPro11M4, 654 | .iPadPro13M4, 655 | ] 656 | for device in Device.allRealDevices { 657 | XCTAssertTrue(device.hasLidarSensor == device.isOneOf(lidarDevices), "testLidarValues failed for \(device.description)") 658 | } 659 | } 660 | 661 | func testHasUSBCConnectivity() { 662 | let usbCDevices: [Device] = [ 663 | .iPhone15, 664 | .iPhone15Plus, 665 | .iPhone15Pro, 666 | .iPhone15ProMax, 667 | .iPhone16, 668 | .iPhone16Plus, 669 | .iPhone16Pro, 670 | .iPhone16ProMax, 671 | .iPhone16e, 672 | .iPad10, 673 | .iPadA16, 674 | .iPadAir4, 675 | .iPadAir5, 676 | .iPadAir11M2, 677 | .iPadAir13M2, 678 | .iPadAir11M3, 679 | .iPadAir13M3, 680 | .iPadMini6, 681 | .iPadMiniA17Pro, 682 | .iPadPro11Inch, 683 | .iPadPro12Inch3, 684 | .iPadPro11Inch2, 685 | .iPadPro12Inch4, 686 | .iPadPro11Inch3, 687 | .iPadPro12Inch5, 688 | .iPadPro11Inch4, 689 | .iPadPro12Inch6, 690 | .iPadPro11M4, 691 | .iPadPro13M4, 692 | ] 693 | for device in Device.allRealDevices { 694 | XCTAssertTrue(device.hasUSBCConnectivity == device.isOneOf(usbCDevices), "testHasUSBCConnectivity failed for \(device.description)") 695 | } 696 | } 697 | 698 | #endif 699 | 700 | // MARK: - tvOS 701 | #if os(tvOS) 702 | 703 | func testDescriptionFromIdentifier() { 704 | XCTAssertEqual(Device.mapToDevice(identifier: "AppleTV5,3").description, "Apple TV HD") 705 | XCTAssertEqual(Device.mapToDevice(identifier: "AppleTV6,2").description, "Apple TV 4K") 706 | } 707 | 708 | func testSafeDescription() { 709 | for device in Device.allRealDevices { 710 | XCTAssertEqual(device.description, device.safeDescription) 711 | } 712 | } 713 | 714 | /// Test that all the ppi values for applicable devices match the public information available at wikipedia. Test non-applicable devices return nil. 715 | func testPPI() { 716 | // Non-applicable devices: 717 | // Apple TV 718 | XCTAssertEqual(Device.appleTVHD.ppi, nil) 719 | XCTAssertEqual(Device.appleTV4K.ppi, nil) 720 | // Simulators 721 | XCTAssertEqual(Device.simulator(Device.appleTVHD).ppi, nil) 722 | XCTAssertEqual(Device.simulator(Device.appleTV4K).ppi, nil) 723 | } 724 | 725 | #endif 726 | 727 | } 728 | -------------------------------------------------------------------------------- /Utils/gyb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import gyb 3 | gyb.main() 4 | -------------------------------------------------------------------------------- /Utils/gyb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # GYB: Generate Your Boilerplate (improved names welcome; at least 3 | # this one's short). See -h output for instructions 4 | 5 | from __future__ import print_function 6 | 7 | import os 8 | import re 9 | try: 10 | from cStringIO import StringIO 11 | except ImportError: 12 | from io import StringIO 13 | import textwrap 14 | import tokenize 15 | 16 | from bisect import bisect 17 | 18 | try: 19 | basestring 20 | except NameError: 21 | basestring = str 22 | 23 | 24 | def get_line_starts(s): 25 | """Return a list containing the start index of each line in s. 26 | 27 | The list also contains a sentinel index for the end of the string, 28 | so there will be one more element in the list than there are lines 29 | in the string 30 | """ 31 | starts = [0] 32 | 33 | for line in s.split('\n'): 34 | starts.append(starts[-1] + len(line) + 1) 35 | 36 | starts[-1] -= 1 37 | return starts 38 | 39 | 40 | def strip_trailing_nl(s): 41 | """If s ends with a newline, drop it; else return s intact""" 42 | return s[:-1] if s.endswith('\n') else s 43 | 44 | 45 | def split_lines(s): 46 | """Split s into a list of lines, each of which has a trailing newline 47 | 48 | If the lines are later concatenated, the result is s, possibly 49 | with a single appended newline. 50 | """ 51 | return [l + '\n' for l in s.split('\n')] 52 | 53 | 54 | # text on a line up to the first '$$', '${', or '%%' 55 | literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' 56 | 57 | # The part of an '%end' line that follows the '%' sign 58 | linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' 59 | 60 | # Note: Where "# Absorb" appears below, the regexp attempts to eat up 61 | # through the end of ${...} and %{...}% constructs. In reality we 62 | # handle this with the Python tokenizer, which avoids mis-detections 63 | # due to nesting, comments and strings. This extra absorption in the 64 | # regexp facilitates testing the regexp on its own, by preventing the 65 | # interior of some of these constructs from being treated as literal 66 | # text. 67 | tokenize_re = re.compile( 68 | r''' 69 | # %-lines and %{...}-blocks 70 | # \n? # absorb one preceding newline 71 | ^ 72 | (?: 73 | (?P 74 | (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + 75 | linesClose + r''' ) .* 76 | ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * 77 | ) 78 | | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) 79 | | [\ \t]* (?P %\{ ) 80 | (?: [^}]| \} (?!%) )* \}% # Absorb 81 | ) 82 | \n? # absorb one trailing newline 83 | 84 | # Substitutions 85 | | (?P \$\{ ) 86 | [^}]* \} # Absorb 87 | 88 | # %% and $$ are literal % and $ respectively 89 | | (?P[$%]) (?P=symbol) 90 | 91 | # Literal text 92 | | (?P ''' + literalText + r''' 93 | (?: 94 | # newline that doesn't precede space+% 95 | (?: \n (?! [\ \t]* %[^%] ) ) 96 | ''' + literalText + r''' 97 | )* 98 | \n? 99 | ) 100 | ''', re.VERBOSE | re.MULTILINE) 101 | 102 | gyb_block_close = re.compile('\}%[ \t]*\n?') 103 | 104 | 105 | def token_pos_to_index(token_pos, start, line_starts): 106 | """Translate a tokenize (line, column) pair into an absolute 107 | position in source text given the position where we started 108 | tokenizing and a list that maps lines onto their starting 109 | character indexes. 110 | """ 111 | relative_token_line_plus1, token_col = token_pos 112 | 113 | # line number where we started tokenizing 114 | start_line_num = bisect(line_starts, start) - 1 115 | 116 | # line number of the token in the whole text 117 | abs_token_line = relative_token_line_plus1 - 1 + start_line_num 118 | 119 | # if found in the first line, adjust the end column to account 120 | # for the extra text 121 | if relative_token_line_plus1 == 1: 122 | token_col += start - line_starts[start_line_num] 123 | 124 | # Sometimes tokenizer errors report a line beyond the last one 125 | if abs_token_line >= len(line_starts): 126 | return line_starts[-1] 127 | 128 | return line_starts[abs_token_line] + token_col 129 | 130 | 131 | def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): 132 | """Apply Python's tokenize to source_text starting at index start 133 | while matching open and close curly braces. When an unmatched 134 | close curly brace is found, return its index. If not found, 135 | return len(source_text). If there's a tokenization error, return 136 | the position of the error. 137 | """ 138 | stream = StringIO(source_text) 139 | stream.seek(start) 140 | nesting = 0 141 | 142 | try: 143 | for kind, text, token_start, token_end, line_text \ 144 | in tokenize.generate_tokens(stream.readline): 145 | 146 | if text == '{': 147 | nesting += 1 148 | elif text == '}': 149 | nesting -= 1 150 | if nesting < 0: 151 | return token_pos_to_index(token_start, start, line_starts) 152 | 153 | except tokenize.TokenError as error: 154 | (message, error_pos) = error.args 155 | return token_pos_to_index(error_pos, start, line_starts) 156 | 157 | return len(source_text) 158 | 159 | 160 | def tokenize_template(template_text): 161 | r"""Given the text of a template, returns an iterator over 162 | (tokenType, token, match) tuples. 163 | 164 | **Note**: this is template syntax tokenization, not Python 165 | tokenization. 166 | 167 | When a non-literal token is matched, a client may call 168 | iter.send(pos) on the iterator to reset the position in 169 | template_text at which scanning will resume. 170 | 171 | This function provides a base level of tokenization which is 172 | then refined by ParseContext.token_generator. 173 | 174 | >>> from pprint import * 175 | >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( 176 | ... '%for x in range(10):\n% print x\n%end\njuicebox'))) 177 | [('gybLines', '%for x in range(10):\n% print x'), 178 | ('gybLinesClose', '%end'), 179 | ('literal', 'juicebox')] 180 | 181 | >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( 182 | ... '''Nothing 183 | ... % if x: 184 | ... % for i in range(3): 185 | ... ${i} 186 | ... % end 187 | ... % else: 188 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 189 | ... '''))) 190 | [('literal', 'Nothing\n'), 191 | ('gybLines', '% if x:\n% for i in range(3):'), 192 | ('substitutionOpen', '${'), 193 | ('literal', '\n'), 194 | ('gybLinesClose', '% end'), 195 | ('gybLines', '% else:'), 196 | ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] 197 | 198 | >>> for kind, text, _ in tokenize_template(''' 199 | ... This is $some$ literal stuff containing a ${substitution} 200 | ... followed by a %{...} block: 201 | ... %{ 202 | ... # Python code 203 | ... }% 204 | ... and here $${are} some %-lines: 205 | ... % x = 1 206 | ... % y = 2 207 | ... % if z == 3: 208 | ... % print '${hello}' 209 | ... % end 210 | ... % for x in zz: 211 | ... % print x 212 | ... % # different indentation 213 | ... % twice 214 | ... and some lines that literally start with a %% token 215 | ... %% first line 216 | ... %% second line 217 | ... '''): 218 | ... print((kind, text.strip().split('\n',1)[0])) 219 | ('literal', 'This is $some$ literal stuff containing a') 220 | ('substitutionOpen', '${') 221 | ('literal', 'followed by a %{...} block:') 222 | ('gybBlockOpen', '%{') 223 | ('literal', 'and here ${are} some %-lines:') 224 | ('gybLines', '% x = 1') 225 | ('gybLinesClose', '% end') 226 | ('gybLines', '% for x in zz:') 227 | ('gybLines', '% # different indentation') 228 | ('gybLines', '% twice') 229 | ('literal', 'and some lines that literally start with a % token') 230 | """ 231 | pos = 0 232 | end = len(template_text) 233 | 234 | saved_literal = [] 235 | literal_first_match = None 236 | 237 | while pos < end: 238 | m = tokenize_re.match(template_text, pos, end) 239 | 240 | # pull out the one matched key (ignoring internal patterns starting 241 | # with _) 242 | ((kind, text), ) = ( 243 | (kind, text) for (kind, text) in m.groupdict().items() 244 | if text is not None and kind[0] != '_') 245 | 246 | if kind in ('literal', 'symbol'): 247 | if len(saved_literal) == 0: 248 | literal_first_match = m 249 | # literals and symbols get batched together 250 | saved_literal.append(text) 251 | pos = None 252 | else: 253 | # found a non-literal. First yield any literal we've accumulated 254 | if saved_literal != []: 255 | yield 'literal', ''.join(saved_literal), literal_first_match 256 | saved_literal = [] 257 | 258 | # Then yield the thing we found. If we get a reply, it's 259 | # the place to resume tokenizing 260 | pos = yield kind, text, m 261 | 262 | # If we were not sent a new position by our client, resume 263 | # tokenizing at the end of this match. 264 | if pos is None: 265 | pos = m.end(0) 266 | else: 267 | # Client is not yet ready to process next token 268 | yield 269 | 270 | if saved_literal != []: 271 | yield 'literal', ''.join(saved_literal), literal_first_match 272 | 273 | 274 | def split_gyb_lines(source_lines): 275 | r"""Return a list of lines at which to split the incoming source 276 | 277 | These positions represent the beginnings of python line groups that 278 | will require a matching %end construct if they are to be closed. 279 | 280 | >>> src = split_lines('''\ 281 | ... if x: 282 | ... print x 283 | ... if y: # trailing comment 284 | ... print z 285 | ... if z: # another comment\ 286 | ... ''') 287 | >>> s = split_gyb_lines(src) 288 | >>> len(s) 289 | 2 290 | >>> src[s[0]] 291 | ' print z\n' 292 | >>> s[1] - len(src) 293 | 0 294 | 295 | >>> src = split_lines('''\ 296 | ... if x: 297 | ... if y: print 1 298 | ... if z: 299 | ... print 2 300 | ... pass\ 301 | ... ''') 302 | >>> s = split_gyb_lines(src) 303 | >>> len(s) 304 | 1 305 | >>> src[s[0]] 306 | ' if y: print 1\n' 307 | 308 | >>> src = split_lines('''\ 309 | ... if x: 310 | ... if y: 311 | ... print 1 312 | ... print 2 313 | ... ''') 314 | >>> s = split_gyb_lines(src) 315 | >>> len(s) 316 | 2 317 | >>> src[s[0]] 318 | ' if y:\n' 319 | >>> src[s[1]] 320 | ' print 1\n' 321 | """ 322 | last_token_text, last_token_kind = None, None 323 | unmatched_indents = [] 324 | 325 | dedents = 0 326 | try: 327 | for token_kind, token_text, token_start, \ 328 | (token_end_line, token_end_col), line_text \ 329 | in tokenize.generate_tokens(lambda i=iter(source_lines): 330 | next(i)): 331 | 332 | if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): 333 | continue 334 | 335 | if token_text == '\n' and last_token_text == ':': 336 | unmatched_indents.append(token_end_line) 337 | 338 | # The tokenizer appends dedents at EOF; don't consider 339 | # those as matching indentations. Instead just save them 340 | # up... 341 | if last_token_kind == tokenize.DEDENT: 342 | dedents += 1 343 | # And count them later, when we see something real. 344 | if token_kind != tokenize.DEDENT and dedents > 0: 345 | unmatched_indents = unmatched_indents[:-dedents] 346 | dedents = 0 347 | 348 | last_token_text, last_token_kind = token_text, token_kind 349 | 350 | except tokenize.TokenError: 351 | # Let the later compile() call report the error 352 | return [] 353 | 354 | if last_token_text == ':': 355 | unmatched_indents.append(len(source_lines)) 356 | 357 | return unmatched_indents 358 | 359 | 360 | def code_starts_with_dedent_keyword(source_lines): 361 | r"""Return True iff the incoming Python source_lines begin with "else", 362 | "elif", "except", or "finally". 363 | 364 | Initial comments and whitespace are ignored. 365 | 366 | >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) 367 | False 368 | >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) 369 | True 370 | >>> code_starts_with_dedent_keyword( 371 | ... split_lines('\n# comment\nelse: # yes')) 372 | True 373 | """ 374 | token_text = None 375 | for token_kind, token_text, _, _, _ \ 376 | in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): 377 | 378 | if token_kind != tokenize.COMMENT and token_text.strip() != '': 379 | break 380 | 381 | return token_text in ('else', 'elif', 'except', 'finally') 382 | 383 | 384 | class ParseContext(object): 385 | 386 | """State carried through a parse of a template""" 387 | 388 | filename = '' 389 | template = '' 390 | line_starts = [] 391 | code_start_line = -1 392 | code_text = None 393 | tokens = None # The rest of the tokens 394 | close_lines = False 395 | 396 | def __init__(self, filename, template=None): 397 | self.filename = os.path.abspath(filename) 398 | if template is None: 399 | with open(filename) as f: 400 | self.template = f.read() 401 | else: 402 | self.template = template 403 | self.line_starts = get_line_starts(self.template) 404 | self.tokens = self.token_generator(tokenize_template(self.template)) 405 | self.next_token() 406 | 407 | def pos_to_line(self, pos): 408 | return bisect(self.line_starts, pos) - 1 409 | 410 | def token_generator(self, base_tokens): 411 | r"""Given an iterator over (kind, text, match) triples (see 412 | tokenize_template above), return a refined iterator over 413 | token_kinds. 414 | 415 | Among other adjustments to the elements found by base_tokens, 416 | this refined iterator tokenizes python code embedded in 417 | template text to help determine its true extent. The 418 | expression "base_tokens.send(pos)" is used to reset the index at 419 | which base_tokens resumes scanning the underlying text. 420 | 421 | >>> ctx = ParseContext('dummy', ''' 422 | ... %for x in y: 423 | ... % print x 424 | ... % end 425 | ... literally 426 | ... ''') 427 | >>> while ctx.token_kind: 428 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 429 | ... ignored = ctx.next_token() 430 | ('literal', '\n') 431 | ('gybLinesOpen', 'for x in y:\n') 432 | ('gybLines', ' print x\n') 433 | ('gybLinesClose', '% end') 434 | ('literal', 'literally\n') 435 | 436 | >>> ctx = ParseContext('dummy', 437 | ... '''Nothing 438 | ... % if x: 439 | ... % for i in range(3): 440 | ... ${i} 441 | ... % end 442 | ... % else: 443 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 444 | ... ''') 445 | >>> while ctx.token_kind: 446 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 447 | ... ignored = ctx.next_token() 448 | ('literal', 'Nothing\n') 449 | ('gybLinesOpen', 'if x:\n') 450 | ('gybLinesOpen', ' for i in range(3):\n') 451 | ('substitutionOpen', 'i') 452 | ('literal', '\n') 453 | ('gybLinesClose', '% end') 454 | ('gybLinesOpen', 'else:\n') 455 | ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') 456 | 457 | >>> ctx = ParseContext('dummy', 458 | ... '''% for x in [1, 2, 3]: 459 | ... % if x == 1: 460 | ... literal1 461 | ... % elif x > 1: # add output line here to fix bug 462 | ... % if x == 2: 463 | ... literal2 464 | ... % end 465 | ... % end 466 | ... % end 467 | ... ''') 468 | >>> while ctx.token_kind: 469 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 470 | ... ignored = ctx.next_token() 471 | ('gybLinesOpen', 'for x in [1, 2, 3]:\n') 472 | ('gybLinesOpen', ' if x == 1:\n') 473 | ('literal', 'literal1\n') 474 | ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') 475 | ('gybLinesOpen', ' if x == 2:\n') 476 | ('literal', 'literal2\n') 477 | ('gybLinesClose', '% end') 478 | ('gybLinesClose', '% end') 479 | ('gybLinesClose', '% end') 480 | """ 481 | for self.token_kind, self.token_text, self.token_match in base_tokens: 482 | kind = self.token_kind 483 | self.code_text = None 484 | 485 | # Do we need to close the current lines? 486 | self.close_lines = kind == 'gybLinesClose' 487 | 488 | # %{...}% and ${...} constructs 489 | if kind.endswith('Open'): 490 | 491 | # Tokenize text that follows as Python up to an unmatched '}' 492 | code_start = self.token_match.end(kind) 493 | self.code_start_line = self.pos_to_line(code_start) 494 | 495 | close_pos = tokenize_python_to_unmatched_close_curly( 496 | self.template, code_start, self.line_starts) 497 | self.code_text = self.template[code_start:close_pos] 498 | yield kind 499 | 500 | if (kind == 'gybBlockOpen'): 501 | # Absorb any '}% \n' 502 | m2 = gyb_block_close.match(self.template, close_pos) 503 | if not m2: 504 | raise ValueError("Invalid block closure") 505 | next_pos = m2.end(0) 506 | else: 507 | assert kind == 'substitutionOpen' 508 | # skip past the closing '}' 509 | next_pos = close_pos + 1 510 | 511 | # Resume tokenizing after the end of the code. 512 | base_tokens.send(next_pos) 513 | 514 | elif kind == 'gybLines': 515 | 516 | self.code_start_line = self.pos_to_line( 517 | self.token_match.start('gybLines')) 518 | indentation = self.token_match.group('_indent') 519 | 520 | # Strip off the leading indentation and %-sign 521 | source_lines = re.split( 522 | '^' + re.escape(indentation), 523 | self.token_match.group('gybLines') + '\n', 524 | flags=re.MULTILINE)[1:] 525 | 526 | if code_starts_with_dedent_keyword(source_lines): 527 | self.close_lines = True 528 | 529 | last_split = 0 530 | for line in split_gyb_lines(source_lines): 531 | self.token_kind = 'gybLinesOpen' 532 | self.code_text = ''.join(source_lines[last_split:line]) 533 | yield self.token_kind 534 | last_split = line 535 | self.code_start_line += line - last_split 536 | self.close_lines = False 537 | 538 | self.code_text = ''.join(source_lines[last_split:]) 539 | if self.code_text: 540 | self.token_kind = 'gybLines' 541 | yield self.token_kind 542 | else: 543 | yield self.token_kind 544 | 545 | def next_token(self): 546 | """Move to the next token""" 547 | for kind in self.tokens: 548 | return self.token_kind 549 | 550 | self.token_kind = None 551 | 552 | 553 | _default_line_directive = '// ###sourceLocation' 554 | 555 | 556 | class ExecutionContext(object): 557 | 558 | """State we pass around during execution of a template""" 559 | 560 | def __init__(self, line_directive=_default_line_directive, 561 | **local_bindings): 562 | self.local_bindings = local_bindings 563 | self.line_directive = line_directive 564 | self.local_bindings['__context__'] = self 565 | self.result_text = [] 566 | self.last_file_line = None 567 | 568 | def append_text(self, text, file, line): 569 | # see if we need to inject a line marker 570 | if self.line_directive: 571 | if (file, line) != self.last_file_line: 572 | # We can only insert the line directive at a line break 573 | if len(self.result_text) == 0 \ 574 | or self.result_text[-1].endswith('\n'): 575 | self.result_text.append('%s(file: "%s", line: %d)\n' % ( 576 | self.line_directive, file, line + 1)) 577 | # But if the new text contains any line breaks, we can create 578 | # one 579 | elif '\n' in text: 580 | i = text.find('\n') 581 | self.result_text.append(text[:i + 1]) 582 | # and try again 583 | self.append_text(text[i + 1:], file, line + 1) 584 | return 585 | 586 | self.result_text.append(text) 587 | self.last_file_line = (file, line + text.count('\n')) 588 | 589 | 590 | class ASTNode(object): 591 | 592 | """Abstract base class for template AST nodes""" 593 | 594 | def __init__(self): 595 | raise NotImplementedError("ASTNode.__init__ is not implemented.") 596 | 597 | def execute(self, context): 598 | raise NotImplementedError("ASTNode.execute is not implemented.") 599 | 600 | def __str__(self, indent=''): 601 | raise NotImplementedError("ASTNode.__str__ is not implemented.") 602 | 603 | def format_children(self, indent): 604 | if not self.children: 605 | return ' []' 606 | 607 | return '\n'.join( 608 | ['', indent + '['] + 609 | [x.__str__(indent + 4 * ' ') for x in self.children] + 610 | [indent + ']']) 611 | 612 | 613 | class Block(ASTNode): 614 | 615 | """A sequence of other AST nodes, to be executed in order""" 616 | 617 | children = [] 618 | 619 | def __init__(self, context): 620 | self.children = [] 621 | 622 | while context.token_kind and not context.close_lines: 623 | if context.token_kind == 'literal': 624 | node = Literal 625 | else: 626 | node = Code 627 | self.children.append(node(context)) 628 | 629 | def execute(self, context): 630 | for x in self.children: 631 | x.execute(context) 632 | 633 | def __str__(self, indent=''): 634 | return indent + 'Block:' + self.format_children(indent) 635 | 636 | 637 | class Literal(ASTNode): 638 | 639 | """An AST node that generates literal text""" 640 | 641 | def __init__(self, context): 642 | self.text = context.token_text 643 | start_position = context.token_match.start(context.token_kind) 644 | self.start_line_number = context.pos_to_line(start_position) 645 | self.filename = context.filename 646 | context.next_token() 647 | 648 | def execute(self, context): 649 | context.append_text(self.text, self.filename, self.start_line_number) 650 | 651 | def __str__(self, indent=''): 652 | return '\n'.join( 653 | [indent + x for x in ['Literal:'] + 654 | strip_trailing_nl(self.text).split('\n')]) 655 | 656 | 657 | class Code(ASTNode): 658 | 659 | """An AST node that is evaluated as Python""" 660 | 661 | code = None 662 | children = () 663 | kind = None 664 | 665 | def __init__(self, context): 666 | 667 | source = '' 668 | source_line_count = 0 669 | 670 | def accumulate_code(): 671 | s = source + (context.code_start_line - source_line_count) * '\n' \ 672 | + textwrap.dedent(context.code_text) 673 | line_count = context.code_start_line + \ 674 | context.code_text.count('\n') 675 | context.next_token() 676 | return s, line_count 677 | 678 | eval_exec = 'exec' 679 | if context.token_kind.startswith('substitution'): 680 | eval_exec = 'eval' 681 | source, source_line_count = accumulate_code() 682 | source = '(' + source.strip() + ')' 683 | 684 | else: 685 | while context.token_kind == 'gybLinesOpen': 686 | source, source_line_count = accumulate_code() 687 | source += ' __children__[%d].execute(__context__)\n' % len( 688 | self.children) 689 | source_line_count += 1 690 | 691 | self.children += (Block(context),) 692 | 693 | if context.token_kind == 'gybLinesClose': 694 | context.next_token() 695 | 696 | if context.token_kind == 'gybLines': 697 | source, source_line_count = accumulate_code() 698 | 699 | # Only handle a substitution as part of this code block if 700 | # we don't already have some %-lines. 701 | elif context.token_kind == 'gybBlockOpen': 702 | 703 | # Opening ${...} and %{...}% constructs 704 | source, source_line_count = accumulate_code() 705 | 706 | self.filename = context.filename 707 | self.start_line_number = context.code_start_line 708 | self.code = compile(source, context.filename, eval_exec) 709 | self.source = source 710 | 711 | def execute(self, context): 712 | # Save __children__ from the local bindings 713 | save_children = context.local_bindings.get('__children__') 714 | # Execute the code with our __children__ in scope 715 | context.local_bindings['__children__'] = self.children 716 | context.local_bindings['__file__'] = self.filename 717 | result = eval(self.code, context.local_bindings) 718 | 719 | if context.local_bindings['__children__'] is not self.children: 720 | raise ValueError("The code is not allowed to mutate __children__") 721 | # Restore the bindings 722 | context.local_bindings['__children__'] = save_children 723 | 724 | # If we got a result, the code was an expression, so append 725 | # its value 726 | if result is not None \ 727 | or (isinstance(result, basestring) and result != ''): 728 | from numbers import Number, Integral 729 | result_string = None 730 | if isinstance(result, Number) and not isinstance(result, Integral): 731 | result_string = repr(result) 732 | else: 733 | result_string = str(result) 734 | context.append_text( 735 | result_string, self.filename, self.start_line_number) 736 | 737 | def __str__(self, indent=''): 738 | source_lines = re.sub(r'^\n', '', strip_trailing_nl( 739 | self.source), flags=re.MULTILINE).split('\n') 740 | if len(source_lines) == 1: 741 | s = indent + 'Code: {' + source_lines[0] + '}' 742 | else: 743 | s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( 744 | indent + 4 * ' ' + l for l in source_lines 745 | ) + '\n' + indent + '}' 746 | return s + self.format_children(indent) 747 | 748 | 749 | def expand(filename, line_directive=_default_line_directive, **local_bindings): 750 | r"""Return the contents of the givepn template file, executed with the given 751 | local bindings. 752 | 753 | >>> from tempfile import NamedTemporaryFile 754 | >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open 755 | >>> # the file for a second time if delete=True. Therefore, we have to 756 | >>> # manually handle closing and deleting this file to allow us to open 757 | >>> # the file by its name across all platforms. 758 | >>> f = NamedTemporaryFile(delete=False) 759 | >>> f.write( 760 | ... r'''--- 761 | ... % for i in range(int(x)): 762 | ... a pox on ${i} for epoxy 763 | ... % end 764 | ... ${120 + 765 | ... 766 | ... 3} 767 | ... abc 768 | ... ${"w\nx\nX\ny"} 769 | ... z 770 | ... ''') 771 | >>> f.flush() 772 | >>> result = expand( 773 | ... f.name, line_directive='//#sourceLocation', x=2 774 | ... ).replace( 775 | ... '"%s"' % f.name, '"dummy.file"') 776 | >>> print(result, end='') 777 | //#sourceLocation(file: "dummy.file", line: 1) 778 | --- 779 | //#sourceLocation(file: "dummy.file", line: 3) 780 | a pox on 0 for epoxy 781 | //#sourceLocation(file: "dummy.file", line: 3) 782 | a pox on 1 for epoxy 783 | //#sourceLocation(file: "dummy.file", line: 5) 784 | 123 785 | //#sourceLocation(file: "dummy.file", line: 8) 786 | abc 787 | w 788 | x 789 | X 790 | y 791 | //#sourceLocation(file: "dummy.file", line: 10) 792 | z 793 | >>> f.close() 794 | >>> os.remove(f.name) 795 | """ 796 | with open(filename) as f: 797 | t = parse_template(filename, f.read()) 798 | d = os.getcwd() 799 | os.chdir(os.path.dirname(os.path.abspath(filename))) 800 | try: 801 | return execute_template( 802 | t, line_directive=line_directive, **local_bindings) 803 | finally: 804 | os.chdir(d) 805 | 806 | 807 | def parse_template(filename, text=None): 808 | r"""Return an AST corresponding to the given template file. 809 | 810 | If text is supplied, it is assumed to be the contents of the file, 811 | as a string. 812 | 813 | >>> print(parse_template('dummy.file', text= 814 | ... '''% for x in [1, 2, 3]: 815 | ... % if x == 1: 816 | ... literal1 817 | ... % elif x > 1: # add output line after this line to fix bug 818 | ... % if x == 2: 819 | ... literal2 820 | ... % end 821 | ... % end 822 | ... % end 823 | ... ''')) 824 | Block: 825 | [ 826 | Code: 827 | { 828 | for x in [1, 2, 3]: 829 | __children__[0].execute(__context__) 830 | } 831 | [ 832 | Block: 833 | [ 834 | Code: 835 | { 836 | if x == 1: 837 | __children__[0].execute(__context__) 838 | elif x > 1: # add output line after this line to fix bug 839 | __children__[1].execute(__context__) 840 | } 841 | [ 842 | Block: 843 | [ 844 | Literal: 845 | literal1 846 | ] 847 | Block: 848 | [ 849 | Code: 850 | { 851 | if x == 2: 852 | __children__[0].execute(__context__) 853 | } 854 | [ 855 | Block: 856 | [ 857 | Literal: 858 | literal2 859 | ] 860 | ] 861 | ] 862 | ] 863 | ] 864 | ] 865 | ] 866 | 867 | >>> print(parse_template( 868 | ... 'dummy.file', 869 | ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) 870 | Block: 871 | [ 872 | Code: 873 | { 874 | for x in range(10): 875 | __children__[0].execute(__context__) 876 | } 877 | [ 878 | Block: 879 | [ 880 | Code: {print(x)} [] 881 | ] 882 | ] 883 | Literal: 884 | juicebox 885 | ] 886 | 887 | >>> print(parse_template('/dummy.file', text= 888 | ... '''Nothing 889 | ... % if x: 890 | ... % for i in range(3): 891 | ... ${i} 892 | ... % end 893 | ... % else: 894 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 895 | ... ''')) 896 | Block: 897 | [ 898 | Literal: 899 | Nothing 900 | Code: 901 | { 902 | if x: 903 | __children__[0].execute(__context__) 904 | else: 905 | __children__[1].execute(__context__) 906 | } 907 | [ 908 | Block: 909 | [ 910 | Code: 911 | { 912 | for i in range(3): 913 | __children__[0].execute(__context__) 914 | } 915 | [ 916 | Block: 917 | [ 918 | Code: {(i)} [] 919 | Literal: 920 | 921 | ] 922 | ] 923 | ] 924 | Block: 925 | [ 926 | Literal: 927 | THIS SHOULD NOT APPEAR IN THE OUTPUT 928 | ] 929 | ] 930 | ] 931 | 932 | >>> print(parse_template('dummy.file', text='''% 933 | ... %for x in y: 934 | ... % print(y) 935 | ... ''')) 936 | Block: 937 | [ 938 | Code: 939 | { 940 | for x in y: 941 | __children__[0].execute(__context__) 942 | } 943 | [ 944 | Block: 945 | [ 946 | Code: {print(y)} [] 947 | ] 948 | ] 949 | ] 950 | 951 | >>> print(parse_template('dummy.file', text='''% 952 | ... %if x: 953 | ... % print(y) 954 | ... AAAA 955 | ... %else: 956 | ... BBBB 957 | ... ''')) 958 | Block: 959 | [ 960 | Code: 961 | { 962 | if x: 963 | __children__[0].execute(__context__) 964 | else: 965 | __children__[1].execute(__context__) 966 | } 967 | [ 968 | Block: 969 | [ 970 | Code: {print(y)} [] 971 | Literal: 972 | AAAA 973 | ] 974 | Block: 975 | [ 976 | Literal: 977 | BBBB 978 | ] 979 | ] 980 | ] 981 | 982 | >>> print(parse_template('dummy.file', text='''% 983 | ... %if x: 984 | ... % print(y) 985 | ... AAAA 986 | ... %# This is a comment 987 | ... %else: 988 | ... BBBB 989 | ... ''')) 990 | Block: 991 | [ 992 | Code: 993 | { 994 | if x: 995 | __children__[0].execute(__context__) 996 | # This is a comment 997 | else: 998 | __children__[1].execute(__context__) 999 | } 1000 | [ 1001 | Block: 1002 | [ 1003 | Code: {print(y)} [] 1004 | Literal: 1005 | AAAA 1006 | ] 1007 | Block: 1008 | [ 1009 | Literal: 1010 | BBBB 1011 | ] 1012 | ] 1013 | ] 1014 | 1015 | >>> print(parse_template('dummy.file', text='''\ 1016 | ... %for x in y: 1017 | ... AAAA 1018 | ... %if x: 1019 | ... BBBB 1020 | ... %end 1021 | ... CCCC 1022 | ... ''')) 1023 | Block: 1024 | [ 1025 | Code: 1026 | { 1027 | for x in y: 1028 | __children__[0].execute(__context__) 1029 | } 1030 | [ 1031 | Block: 1032 | [ 1033 | Literal: 1034 | AAAA 1035 | Code: 1036 | { 1037 | if x: 1038 | __children__[0].execute(__context__) 1039 | } 1040 | [ 1041 | Block: 1042 | [ 1043 | Literal: 1044 | BBBB 1045 | ] 1046 | ] 1047 | Literal: 1048 | CCCC 1049 | ] 1050 | ] 1051 | ] 1052 | """ 1053 | return Block(ParseContext(filename, text)) 1054 | 1055 | 1056 | def execute_template( 1057 | ast, line_directive=_default_line_directive, **local_bindings): 1058 | r"""Return the text generated by executing the given template AST. 1059 | 1060 | Keyword arguments become local variable bindings in the execution context 1061 | 1062 | >>> root_directory = os.path.abspath('/') 1063 | >>> file_name = root_directory + 'dummy.file' 1064 | >>> ast = parse_template(file_name, text= 1065 | ... '''Nothing 1066 | ... % if x: 1067 | ... % for i in range(3): 1068 | ... ${i} 1069 | ... % end 1070 | ... % else: 1071 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 1072 | ... ''') 1073 | >>> out = execute_template(ast, line_directive='//#sourceLocation', x=1) 1074 | >>> out = out.replace(file_name, "DUMMY-FILE") 1075 | >>> print(out, end="") 1076 | //#sourceLocation(file: "DUMMY-FILE", line: 1) 1077 | Nothing 1078 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1079 | 0 1080 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1081 | 1 1082 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1083 | 2 1084 | 1085 | >>> ast = parse_template(file_name, text= 1086 | ... '''Nothing 1087 | ... % a = [] 1088 | ... % for x in range(3): 1089 | ... % a.append(x) 1090 | ... % end 1091 | ... ${a} 1092 | ... ''') 1093 | >>> out = execute_template(ast, line_directive='//#sourceLocation', x=1) 1094 | >>> out = out.replace(file_name, "DUMMY-FILE") 1095 | >>> print(out, end="") 1096 | //#sourceLocation(file: "DUMMY-FILE", line: 1) 1097 | Nothing 1098 | //#sourceLocation(file: "DUMMY-FILE", line: 6) 1099 | [0, 1, 2] 1100 | """ 1101 | execution_context = ExecutionContext( 1102 | line_directive=line_directive, **local_bindings) 1103 | ast.execute(execution_context) 1104 | return ''.join(execution_context.result_text) 1105 | 1106 | 1107 | def main(): 1108 | """ 1109 | Lint this file. 1110 | >>> import sys 1111 | >>> gyb_path = os.path.realpath(__file__).replace('.pyc', '.py') 1112 | >>> sys.path.append(os.path.dirname(gyb_path)) 1113 | >>> import python_lint 1114 | >>> python_lint.lint([gyb_path], verbose=False) 1115 | 0 1116 | """ 1117 | 1118 | import argparse 1119 | import sys 1120 | 1121 | parser = argparse.ArgumentParser( 1122 | formatter_class=argparse.RawDescriptionHelpFormatter, 1123 | description='Generate Your Boilerplate!', epilog=''' 1124 | A GYB template consists of the following elements: 1125 | 1126 | - Literal text which is inserted directly into the output 1127 | 1128 | - %% or $$ in literal text, which insert literal '%' and '$' 1129 | symbols respectively. 1130 | 1131 | - Substitutions of the form ${}. The Python 1132 | expression is converted to a string and the result is inserted 1133 | into the output. 1134 | 1135 | - Python code delimited by %{...}%. Typically used to inject 1136 | definitions (functions, classes, variable bindings) into the 1137 | evaluation context of the template. Common indentation is 1138 | stripped, so you can add as much indentation to the beginning 1139 | of this code as you like 1140 | 1141 | - Lines beginning with optional whitespace followed by a single 1142 | '%' and Python code. %-lines allow you to nest other 1143 | constructs inside them. To close a level of nesting, use the 1144 | "%end" construct. 1145 | 1146 | - Lines beginning with optional whitespace and followed by a 1147 | single '%' and the token "end", which close open constructs in 1148 | %-lines. 1149 | 1150 | Example template: 1151 | 1152 | - Hello - 1153 | %{ 1154 | x = 42 1155 | def succ(a): 1156 | return a+1 1157 | }% 1158 | 1159 | I can assure you that ${x} < ${succ(x)} 1160 | 1161 | % if int(y) > 7: 1162 | % for i in range(3): 1163 | y is greater than seven! 1164 | % end 1165 | % else: 1166 | y is less than or equal to seven 1167 | % end 1168 | 1169 | - The End. - 1170 | 1171 | When run with "gyb -Dy=9", the output is 1172 | 1173 | - Hello - 1174 | 1175 | I can assure you that 42 < 43 1176 | 1177 | y is greater than seven! 1178 | y is greater than seven! 1179 | y is greater than seven! 1180 | 1181 | - The End. - 1182 | ''' 1183 | ) 1184 | parser.add_argument( 1185 | '-D', action='append', dest='defines', metavar='NAME=VALUE', 1186 | default=[], 1187 | help='''Bindings to be set in the template's execution context''') 1188 | 1189 | parser.add_argument( 1190 | 'file', type=argparse.FileType(), 1191 | help='Path to GYB template file (defaults to stdin)', nargs='?', 1192 | default=sys.stdin) 1193 | parser.add_argument( 1194 | '-o', dest='target', type=argparse.FileType('w'), 1195 | help='Output file (defaults to stdout)', default=sys.stdout) 1196 | parser.add_argument( 1197 | '--test', action='store_true', 1198 | default=False, help='Run a self-test') 1199 | parser.add_argument( 1200 | '--verbose-test', action='store_true', 1201 | default=False, help='Run a verbose self-test') 1202 | parser.add_argument( 1203 | '--dump', action='store_true', 1204 | default=False, help='Dump the parsed template to stdout') 1205 | parser.add_argument( 1206 | '--line-directive', default='// ###sourceLocation', 1207 | help='Line directive prefix; empty => no line markers') 1208 | 1209 | args = parser.parse_args(sys.argv[1:]) 1210 | 1211 | if args.test or args.verbose_test: 1212 | import doctest 1213 | selfmod = sys.modules[__name__] 1214 | if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: 1215 | sys.exit(1) 1216 | 1217 | bindings = dict(x.split('=', 1) for x in args.defines) 1218 | ast = parse_template(args.file.name, args.file.read()) 1219 | if args.dump: 1220 | print(ast) 1221 | # Allow the template to open files and import .py files relative to its own 1222 | # directory 1223 | os.chdir(os.path.dirname(os.path.abspath(args.file.name))) 1224 | sys.path = ['.'] + sys.path 1225 | 1226 | args.target.write(execute_template(ast, args.line_directive, **bindings)) 1227 | 1228 | 1229 | if __name__ == '__main__': 1230 | main() 1231 | --------------------------------------------------------------------------------