├── .gitignore ├── .ruby-version ├── ExampleApplication ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CellType.swift ├── CheapTableViewCell.swift ├── ExampleApplication-Bridging-Header.h ├── ExorbitantTableViewCell.swift ├── ExpensiveTableViewCell.swift ├── Info.plist ├── TableViewController.swift └── TitleConfiguring.swift ├── Gemfile ├── Gemfile.lock ├── KMCGeigerCounter.podspec ├── KMCGeigerCounter.xcodeproj └── project.pbxproj ├── KMCGeigerCounter ├── KMCGeigerCounter.h ├── KMCGeigerCounter.m └── KMCGeigerCounterTick.aiff ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # CocoaPods 23 | Pods 24 | *.xcworkspace 25 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.0 2 | -------------------------------------------------------------------------------- /ExampleApplication/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | final class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | 18 | KMCGeigerCounter.shared().isEnabled = true 19 | 20 | return true 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ExampleApplication/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExampleApplication/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleApplication/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ExampleApplication/CellType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellType.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | enum CellType: Int { 10 | 11 | case cheap = 0 12 | case expensive = 1 13 | case exorbitant = 2 14 | 15 | var cellIdentifier: String { 16 | switch self { 17 | case .cheap: 18 | return "cheap" 19 | case .expensive: 20 | return "expensive" 21 | case .exorbitant: 22 | return "exorbitant" 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ExampleApplication/CheapTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheapTableViewCell.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // This cell is cheap to create since its layout is simple. 12 | // It's also cheap to reuse since its layout does not depend on row data. 13 | 14 | final class CheapTableViewCell: UITableViewCell { 15 | 16 | @IBOutlet private var titleLabel: UILabel! 17 | 18 | } 19 | 20 | extension CheapTableViewCell: RowConfiguring { 21 | 22 | func configure(at index: Int) { 23 | titleLabel.text = "Cheap\nRow \(index)" 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ExampleApplication/ExampleApplication-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "KMCGeigerCounter.h" 6 | -------------------------------------------------------------------------------- /ExampleApplication/ExorbitantTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpensiveTableViewCell.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // This cell is expensive to create since its layout is complex. 12 | // It's also expensive to reuse since its layout depends on row data. 13 | 14 | final class ExorbitantTableViewCell: UITableViewCell { 15 | 16 | @IBOutlet private var rootStackView: UIStackView! 17 | 18 | } 19 | 20 | extension ExorbitantTableViewCell: RowConfiguring { 21 | 22 | func configure(at index: Int) { 23 | let title = "Exorbitant\nRow \(index)" 24 | let spacing = CGFloat(index) 25 | 26 | rootStackView.spacing = spacing 27 | 28 | for case let stackView as UIStackView in rootStackView.arrangedSubviews { 29 | stackView.spacing = spacing 30 | 31 | for case let label as UILabel in stackView.arrangedSubviews { 32 | label.text = title 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ExampleApplication/ExpensiveTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformantTableViewCell 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // This cell is expensive to create since its layout is complex, 12 | // but it's cheap to reuse since its layout does not depend on row data. 13 | 14 | final class ExpensiveTableViewCell: UITableViewCell { 15 | 16 | @IBOutlet private var rootStackView: UIStackView! 17 | 18 | } 19 | 20 | extension ExpensiveTableViewCell: RowConfiguring { 21 | 22 | func configure(at index: Int) { 23 | let title = "Expensive\nRow" 24 | for case let stackView as UIStackView in rootStackView.arrangedSubviews { 25 | for case let label as UILabel in stackView.arrangedSubviews { 26 | label.text = title 27 | } 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ExampleApplication/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ExampleApplication/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class TableViewController: UITableViewController { 12 | 13 | @IBOutlet var cellTypeSegmentedControl: UISegmentedControl! 14 | 15 | // MARK: - UITableViewDataSource 16 | 17 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 18 | return 20 19 | } 20 | 21 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 22 | let cellType = CellType(rawValue: cellTypeSegmentedControl.selectedSegmentIndex) ?? .expensive 23 | let cellIdentifier = cellType.cellIdentifier 24 | 25 | let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) 26 | 27 | if let rowConfiguringCell = cell as? RowConfiguring { 28 | rowConfiguringCell.configure(at: indexPath.row) 29 | } 30 | 31 | return cell 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ExampleApplication/TitleConfiguring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RowConfiguring.swift 3 | // ExampleApplication 4 | // 5 | // Created by Kevin Conner on 9/1/18. 6 | // Copyright © 2018 Kevin Conner. All rights reserved. 7 | // 8 | 9 | protocol RowConfiguring { 10 | 11 | func configure(at index: Int) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.5.3) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.5.3) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.2.0, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.0, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.5) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.1) 30 | xcodeproj (>= 1.5.7, < 2.0) 31 | cocoapods-core (1.5.3) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.2) 36 | cocoapods-downloader (1.6.3) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.0.0) 41 | cocoapods-trunk (1.3.0) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.0.5) 47 | escape (0.0.4) 48 | fourflusher (2.0.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.2.0) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.10) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.5.9) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.2) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.5) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods 74 | 75 | BUNDLED WITH 76 | 1.16.3 77 | -------------------------------------------------------------------------------- /KMCGeigerCounter.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "KMCGeigerCounter" 3 | s.version = "0.3.0" 4 | s.summary = "A framerate meter that clicks when animation drops frames" 5 | s.homepage = "https://github.com/kconner/KMCGeigerCounter" 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { "Kevin Conner" => "connerk@gmail.com" } 8 | s.social_media_url = "http://twitter.com/connerk" 9 | s.platform = :ios, '8.0' 10 | s.source = { :git => "https://github.com/kconner/KMCGeigerCounter.git", :tag => s.version.to_s } 11 | s.requires_arc = true 12 | s.source_files = "KMCGeigerCounter/*.{h,m}" 13 | s.resources = "KMCGeigerCounter/*.aiff" 14 | end 15 | -------------------------------------------------------------------------------- /KMCGeigerCounter.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A7492D2213B1FE800D773B6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492D1213B1FE800D773B6 /* AppDelegate.swift */; }; 11 | 0A7492D7213B1FE800D773B6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0A7492D5213B1FE800D773B6 /* Main.storyboard */; }; 12 | 0A7492D9213B1FE900D773B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A7492D8213B1FE900D773B6 /* Assets.xcassets */; }; 13 | 0A7492DC213B1FE900D773B6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0A7492DA213B1FE900D773B6 /* LaunchScreen.storyboard */; }; 14 | 0A7492E2213B200000D773B6 /* KMCGeigerCounterTick.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 0AAC72F219F7096D00D6522D /* KMCGeigerCounterTick.aiff */; }; 15 | 0A7492EA213B215E00D773B6 /* KMCGeigerCounter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AAC72F419F7096D00D6522D /* KMCGeigerCounter.m */; }; 16 | 0A7492EE213B240100D773B6 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492ED213B240100D773B6 /* TableViewController.swift */; }; 17 | 0A7492F0213B2F5700D773B6 /* ExorbitantTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492EF213B2F5700D773B6 /* ExorbitantTableViewCell.swift */; }; 18 | 0A7492F2213B2F5900D773B6 /* ExpensiveTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492F1213B2F5900D773B6 /* ExpensiveTableViewCell.swift */; }; 19 | 0A7492F4213B2F7A00D773B6 /* TitleConfiguring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492F3213B2F7A00D773B6 /* TitleConfiguring.swift */; }; 20 | 0A7492F6213B304D00D773B6 /* CellType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492F5213B304D00D773B6 /* CellType.swift */; }; 21 | 0A7492F8213B4F5600D773B6 /* CheapTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7492F7213B4F5600D773B6 /* CheapTableViewCell.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 0A7492CF213B1FE800D773B6 /* ExampleApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 0A7492D1213B1FE800D773B6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 27 | 0A7492D6213B1FE800D773B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 0A7492D8213B1FE900D773B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 0A7492DB213B1FE900D773B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 0A7492DD213B1FE900D773B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 0A7492E6213B214700D773B6 /* ExampleApplication-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ExampleApplication-Bridging-Header.h"; sourceTree = ""; }; 32 | 0A7492ED213B240100D773B6 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 33 | 0A7492EF213B2F5700D773B6 /* ExorbitantTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExorbitantTableViewCell.swift; sourceTree = ""; }; 34 | 0A7492F1213B2F5900D773B6 /* ExpensiveTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpensiveTableViewCell.swift; sourceTree = ""; }; 35 | 0A7492F3213B2F7A00D773B6 /* TitleConfiguring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleConfiguring.swift; sourceTree = ""; }; 36 | 0A7492F5213B304D00D773B6 /* CellType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellType.swift; sourceTree = ""; }; 37 | 0A7492F7213B4F5600D773B6 /* CheapTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheapTableViewCell.swift; sourceTree = ""; }; 38 | 0AAC72F219F7096D00D6522D /* KMCGeigerCounterTick.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = KMCGeigerCounterTick.aiff; sourceTree = ""; }; 39 | 0AAC72F319F7096D00D6522D /* KMCGeigerCounter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMCGeigerCounter.h; sourceTree = ""; }; 40 | 0AAC72F419F7096D00D6522D /* KMCGeigerCounter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMCGeigerCounter.m; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 0A7492CC213B1FE800D773B6 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 0A7492D0213B1FE800D773B6 /* ExampleApplication */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 0A7492E6213B214700D773B6 /* ExampleApplication-Bridging-Header.h */, 58 | 0A7492D1213B1FE800D773B6 /* AppDelegate.swift */, 59 | 0A7492F5213B304D00D773B6 /* CellType.swift */, 60 | 0A7492ED213B240100D773B6 /* TableViewController.swift */, 61 | 0A7492F3213B2F7A00D773B6 /* TitleConfiguring.swift */, 62 | 0A7492F7213B4F5600D773B6 /* CheapTableViewCell.swift */, 63 | 0A7492F1213B2F5900D773B6 /* ExpensiveTableViewCell.swift */, 64 | 0A7492EF213B2F5700D773B6 /* ExorbitantTableViewCell.swift */, 65 | 0A7492D5213B1FE800D773B6 /* Main.storyboard */, 66 | 0A7492D8213B1FE900D773B6 /* Assets.xcassets */, 67 | 0A7492DA213B1FE900D773B6 /* LaunchScreen.storyboard */, 68 | 0A7492DD213B1FE900D773B6 /* Info.plist */, 69 | ); 70 | path = ExampleApplication; 71 | sourceTree = ""; 72 | }; 73 | 0AAC72BF19F7085800D6522D = { 74 | isa = PBXGroup; 75 | children = ( 76 | 0AAC72F119F7091800D6522D /* KMCGeigerCounter */, 77 | 0A7492D0213B1FE800D773B6 /* ExampleApplication */, 78 | 0AAC72C919F7085800D6522D /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 0AAC72C919F7085800D6522D /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 0A7492CF213B1FE800D773B6 /* ExampleApplication.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 0AAC72F119F7091800D6522D /* KMCGeigerCounter */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 0AAC72F219F7096D00D6522D /* KMCGeigerCounterTick.aiff */, 94 | 0AAC72F319F7096D00D6522D /* KMCGeigerCounter.h */, 95 | 0AAC72F419F7096D00D6522D /* KMCGeigerCounter.m */, 96 | ); 97 | path = KMCGeigerCounter; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | 0A7492CE213B1FE800D773B6 /* ExampleApplication */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = 0A7492DE213B1FE900D773B6 /* Build configuration list for PBXNativeTarget "ExampleApplication" */; 106 | buildPhases = ( 107 | 0A7492CB213B1FE800D773B6 /* Sources */, 108 | 0A7492CC213B1FE800D773B6 /* Frameworks */, 109 | 0A7492CD213B1FE800D773B6 /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = ExampleApplication; 116 | productName = ExampleApplication; 117 | productReference = 0A7492CF213B1FE800D773B6 /* ExampleApplication.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | 0AAC72C019F7085800D6522D /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 1000; 127 | LastUpgradeCheck = 1000; 128 | ORGANIZATIONNAME = "Kevin Conner"; 129 | TargetAttributes = { 130 | 0A7492CE213B1FE800D773B6 = { 131 | CreatedOnToolsVersion = 10.0; 132 | DevelopmentTeam = VAK384QBBN; 133 | LastSwiftMigration = 1000; 134 | ProvisioningStyle = Automatic; 135 | }; 136 | }; 137 | }; 138 | buildConfigurationList = 0AAC72C319F7085800D6522D /* Build configuration list for PBXProject "KMCGeigerCounter" */; 139 | compatibilityVersion = "Xcode 3.2"; 140 | developmentRegion = English; 141 | hasScannedForEncodings = 0; 142 | knownRegions = ( 143 | en, 144 | Base, 145 | ); 146 | mainGroup = 0AAC72BF19F7085800D6522D; 147 | productRefGroup = 0AAC72C919F7085800D6522D /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | 0A7492CE213B1FE800D773B6 /* ExampleApplication */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | 0A7492CD213B1FE800D773B6 /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 0A7492DC213B1FE900D773B6 /* LaunchScreen.storyboard in Resources */, 162 | 0A7492D9213B1FE900D773B6 /* Assets.xcassets in Resources */, 163 | 0A7492E2213B200000D773B6 /* KMCGeigerCounterTick.aiff in Resources */, 164 | 0A7492D7213B1FE800D773B6 /* Main.storyboard in Resources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXResourcesBuildPhase section */ 169 | 170 | /* Begin PBXSourcesBuildPhase section */ 171 | 0A7492CB213B1FE800D773B6 /* Sources */ = { 172 | isa = PBXSourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | 0A7492F2213B2F5900D773B6 /* ExpensiveTableViewCell.swift in Sources */, 176 | 0A7492EA213B215E00D773B6 /* KMCGeigerCounter.m in Sources */, 177 | 0A7492F0213B2F5700D773B6 /* ExorbitantTableViewCell.swift in Sources */, 178 | 0A7492F4213B2F7A00D773B6 /* TitleConfiguring.swift in Sources */, 179 | 0A7492F6213B304D00D773B6 /* CellType.swift in Sources */, 180 | 0A7492EE213B240100D773B6 /* TableViewController.swift in Sources */, 181 | 0A7492D2213B1FE800D773B6 /* AppDelegate.swift in Sources */, 182 | 0A7492F8213B4F5600D773B6 /* CheapTableViewCell.swift in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 0A7492D5213B1FE800D773B6 /* Main.storyboard */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 0A7492D6213B1FE800D773B6 /* Base */, 193 | ); 194 | name = Main.storyboard; 195 | sourceTree = ""; 196 | }; 197 | 0A7492DA213B1FE900D773B6 /* LaunchScreen.storyboard */ = { 198 | isa = PBXVariantGroup; 199 | children = ( 200 | 0A7492DB213B1FE900D773B6 /* Base */, 201 | ); 202 | name = LaunchScreen.storyboard; 203 | sourceTree = ""; 204 | }; 205 | /* End PBXVariantGroup section */ 206 | 207 | /* Begin XCBuildConfiguration section */ 208 | 0A7492DF213B1FE900D773B6 /* Debug */ = { 209 | isa = XCBuildConfiguration; 210 | buildSettings = { 211 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 212 | CLANG_ANALYZER_NONNULL = YES; 213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 215 | CLANG_ENABLE_MODULES = YES; 216 | CLANG_ENABLE_OBJC_WEAK = YES; 217 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 218 | CLANG_WARN_COMMA = YES; 219 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 220 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 226 | CLANG_WARN_STRICT_PROTOTYPES = YES; 227 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 229 | CODE_SIGN_IDENTITY = "iPhone Developer"; 230 | CODE_SIGN_STYLE = Automatic; 231 | DEBUG_INFORMATION_FORMAT = dwarf; 232 | DEVELOPMENT_TEAM = VAK384QBBN; 233 | ENABLE_TESTABILITY = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_NO_COMMON_BLOCKS = YES; 236 | INFOPLIST_FILE = ExampleApplication/Info.plist; 237 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 238 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 239 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 240 | MTL_FAST_MATH = YES; 241 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinconner.ExampleApplication; 242 | PRODUCT_NAME = "$(TARGET_NAME)"; 243 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 244 | SWIFT_OBJC_BRIDGING_HEADER = "ExampleApplication/ExampleApplication-Bridging-Header.h"; 245 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 246 | SWIFT_VERSION = 4.2; 247 | TARGETED_DEVICE_FAMILY = "1,2"; 248 | }; 249 | name = Debug; 250 | }; 251 | 0A7492E0213B1FE900D773B6 /* Release */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 257 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_WEAK = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_COMMA = YES; 262 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 263 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 269 | CLANG_WARN_STRICT_PROTOTYPES = YES; 270 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 271 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 272 | CODE_SIGN_IDENTITY = "iPhone Developer"; 273 | CODE_SIGN_STYLE = Automatic; 274 | COPY_PHASE_STRIP = NO; 275 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 276 | DEVELOPMENT_TEAM = VAK384QBBN; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | INFOPLIST_FILE = ExampleApplication/Info.plist; 280 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 281 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 282 | MTL_FAST_MATH = YES; 283 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinconner.ExampleApplication; 284 | PRODUCT_NAME = "$(TARGET_NAME)"; 285 | SWIFT_OBJC_BRIDGING_HEADER = "ExampleApplication/ExampleApplication-Bridging-Header.h"; 286 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 287 | SWIFT_VERSION = 4.2; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | }; 290 | name = Release; 291 | }; 292 | 0AAC72E919F7085800D6522D /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ALWAYS_SEARCH_USER_PATHS = NO; 296 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 297 | CLANG_CXX_LIBRARY = "libc++"; 298 | CLANG_ENABLE_MODULES = YES; 299 | CLANG_ENABLE_OBJC_ARC = YES; 300 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 301 | CLANG_WARN_BOOL_CONVERSION = YES; 302 | CLANG_WARN_COMMA = YES; 303 | CLANG_WARN_CONSTANT_CONVERSION = YES; 304 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 305 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 306 | CLANG_WARN_EMPTY_BODY = YES; 307 | CLANG_WARN_ENUM_CONVERSION = YES; 308 | CLANG_WARN_INFINITE_RECURSION = YES; 309 | CLANG_WARN_INT_CONVERSION = YES; 310 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 311 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 312 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 313 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 314 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 315 | CLANG_WARN_STRICT_PROTOTYPES = YES; 316 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 320 | COPY_PHASE_STRIP = NO; 321 | ENABLE_STRICT_OBJC_MSGSEND = YES; 322 | ENABLE_TESTABILITY = YES; 323 | GCC_C_LANGUAGE_STANDARD = gnu99; 324 | GCC_DYNAMIC_NO_PIC = NO; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_OPTIMIZATION_LEVEL = 0; 327 | GCC_PREPROCESSOR_DEFINITIONS = ( 328 | "DEBUG=1", 329 | "$(inherited)", 330 | ); 331 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 334 | GCC_WARN_UNDECLARED_SELECTOR = YES; 335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 336 | GCC_WARN_UNUSED_FUNCTION = YES; 337 | GCC_WARN_UNUSED_VARIABLE = YES; 338 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 339 | MTL_ENABLE_DEBUG_INFO = YES; 340 | ONLY_ACTIVE_ARCH = YES; 341 | SDKROOT = iphoneos; 342 | }; 343 | name = Debug; 344 | }; 345 | 0AAC72EA19F7085800D6522D /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ALWAYS_SEARCH_USER_PATHS = NO; 349 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 350 | CLANG_CXX_LIBRARY = "libc++"; 351 | CLANG_ENABLE_MODULES = YES; 352 | CLANG_ENABLE_OBJC_ARC = YES; 353 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 354 | CLANG_WARN_BOOL_CONVERSION = YES; 355 | CLANG_WARN_COMMA = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 358 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 359 | CLANG_WARN_EMPTY_BODY = YES; 360 | CLANG_WARN_ENUM_CONVERSION = YES; 361 | CLANG_WARN_INFINITE_RECURSION = YES; 362 | CLANG_WARN_INT_CONVERSION = YES; 363 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 365 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 366 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 368 | CLANG_WARN_STRICT_PROTOTYPES = YES; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 373 | COPY_PHASE_STRIP = YES; 374 | ENABLE_NS_ASSERTIONS = NO; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu99; 377 | GCC_NO_COMMON_BLOCKS = YES; 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 385 | MTL_ENABLE_DEBUG_INFO = NO; 386 | SDKROOT = iphoneos; 387 | VALIDATE_PRODUCT = YES; 388 | }; 389 | name = Release; 390 | }; 391 | /* End XCBuildConfiguration section */ 392 | 393 | /* Begin XCConfigurationList section */ 394 | 0A7492DE213B1FE900D773B6 /* Build configuration list for PBXNativeTarget "ExampleApplication" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | 0A7492DF213B1FE900D773B6 /* Debug */, 398 | 0A7492E0213B1FE900D773B6 /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | 0AAC72C319F7085800D6522D /* Build configuration list for PBXProject "KMCGeigerCounter" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | 0AAC72E919F7085800D6522D /* Debug */, 407 | 0AAC72EA19F7085800D6522D /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | /* End XCConfigurationList section */ 413 | }; 414 | rootObject = 0AAC72C019F7085800D6522D /* Project object */; 415 | } 416 | -------------------------------------------------------------------------------- /KMCGeigerCounter/KMCGeigerCounter.h: -------------------------------------------------------------------------------- 1 | // 2 | // KMCGeigerCounter.h 3 | // KMCGeigerCounter 4 | // 5 | // Created by Kevin Conner on 10/21/14. 6 | // Copyright (c) 2014 Kevin Conner. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, KMCGeigerCounterPosition) { 12 | KMCGeigerCounterPositionLeft, 13 | KMCGeigerCounterPositionMiddle, 14 | KMCGeigerCounterPositionRight 15 | }; 16 | 17 | @interface KMCGeigerCounter : NSObject 18 | 19 | // Set [KMCGeigerCounter sharedGeigerCounter].enabled = YES from -application:didFinishLaunchingWithOptions:. 20 | @property (nonatomic, assign, getter = isEnabled) BOOL enabled; 21 | 22 | // The meter draws over the status bar. Set the window level manually if your own custom windows obscure it. 23 | @property (nonatomic, assign) UIWindowLevel windowLevel; 24 | 25 | // Position of the meter in the status bar. Takes effect on next enable. 26 | @property (nonatomic, assign) KMCGeigerCounterPosition position; 27 | 28 | @property (nonatomic, readonly, getter = isRunning) BOOL running; 29 | @property (nonatomic, readonly) NSInteger droppedFrameCountInLastSecond; 30 | @property (nonatomic, readonly) NSInteger drawnFrameCountInLastSecond; // -1 until one second of frames have been collected 31 | 32 | + (instancetype)sharedGeigerCounter; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /KMCGeigerCounter/KMCGeigerCounter.m: -------------------------------------------------------------------------------- 1 | // 2 | // KMCGeigerCounter.m 3 | // KMCGeigerCounter 4 | // 5 | // Created by Kevin Conner on 10/21/14. 6 | // Copyright (c) 2014 Kevin Conner. All rights reserved. 7 | // 8 | 9 | #import "KMCGeigerCounter.h" 10 | #import 11 | 12 | @interface KMCGeigerCounter () 13 | 14 | @property (nonatomic, readwrite, assign, getter = isRunning) BOOL running; 15 | 16 | @property (nonatomic, strong) UIWindow *window; 17 | @property (nonatomic, strong) UILabel *meterLabel; 18 | @property (nonatomic, strong) UIColor *meterPerfectColor; 19 | @property (nonatomic, strong) UIColor *meterGoodColor; 20 | @property (nonatomic, strong) UIColor *meterBadColor; 21 | 22 | @property (nonatomic, strong) CADisplayLink *displayLink; 23 | @property (nonatomic, assign) SystemSoundID tickSoundID; 24 | 25 | @property (nonatomic, assign) NSInteger frameNumber; 26 | 27 | @property (nonatomic, assign) NSInteger hardwareFramesPerSecond; 28 | @property (nonatomic, assign) CFTimeInterval *recentFrameTimes; // malloc: CFTimeInterval[hardwareFramesPerSecond] 29 | 30 | @end 31 | 32 | @implementation KMCGeigerCounter 33 | 34 | #pragma mark - Helpers 35 | 36 | + (UIColor *)colorWithHex:(uint32_t)hex alpha:(CGFloat)alpha 37 | { 38 | CGFloat red = (CGFloat) ((hex & 0xff0000) >> 16) / 255.0f; 39 | CGFloat green = (CGFloat) ((hex & 0x00ff00) >> 8) / 255.0f; 40 | CGFloat blue = (CGFloat) (hex & 0x0000ff) / 255.0f; 41 | return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; 42 | } 43 | 44 | - (CFTimeInterval)lastFrameTime 45 | { 46 | return _recentFrameTimes[self.frameNumber % self.hardwareFramesPerSecond]; 47 | } 48 | 49 | - (void)recordFrameTime:(CFTimeInterval)frameTime 50 | { 51 | ++self.frameNumber; 52 | _recentFrameTimes[self.frameNumber % self.hardwareFramesPerSecond] = frameTime; 53 | } 54 | 55 | - (void)clearLastSecondOfFrameTimes 56 | { 57 | CFTimeInterval initialFrameTime = CACurrentMediaTime(); 58 | for (NSInteger i = 0; i < self.hardwareFramesPerSecond; ++i) { 59 | _recentFrameTimes[i] = initialFrameTime; 60 | } 61 | self.frameNumber = 0; 62 | } 63 | 64 | - (void)updateMeterLabel 65 | { 66 | NSInteger droppedFrameCount = self.droppedFrameCountInLastSecond; 67 | NSInteger drawnFrameCount = self.drawnFrameCountInLastSecond; 68 | 69 | NSString *droppedString; 70 | NSString *drawnString; 71 | 72 | if (droppedFrameCount <= 0) { 73 | self.meterLabel.backgroundColor = self.meterPerfectColor; 74 | 75 | droppedString = @"--"; 76 | } else { 77 | if (droppedFrameCount <= 2) { 78 | self.meterLabel.backgroundColor = self.meterGoodColor; 79 | } else { 80 | self.meterLabel.backgroundColor = self.meterBadColor; 81 | } 82 | 83 | droppedString = [NSString stringWithFormat:@"%ld", (long) droppedFrameCount]; 84 | } 85 | 86 | if (drawnFrameCount == -1) { 87 | drawnString = @"--"; 88 | } else { 89 | drawnString = [NSString stringWithFormat:@"%ld", (long) drawnFrameCount]; 90 | } 91 | 92 | self.meterLabel.text = [NSString stringWithFormat:@"%@ %@", droppedString, drawnString]; 93 | } 94 | 95 | - (CFTimeInterval)hardwareFrameDuration 96 | { 97 | return 1.0 / self.hardwareFramesPerSecond; 98 | } 99 | 100 | - (void)displayLinkWillDraw:(CADisplayLink *)displayLink 101 | { 102 | CFTimeInterval currentFrameTime = displayLink.timestamp; 103 | CFTimeInterval frameDuration = currentFrameTime - [self lastFrameTime]; 104 | 105 | // Frames should be even multiples of hardwareFrameDuration. 106 | // If a frame takes two frame durations, we dropped at least one, so click. 107 | if (1.5 < frameDuration / [self hardwareFrameDuration]) { 108 | AudioServicesPlaySystemSound(self.tickSoundID); 109 | } 110 | 111 | [self recordFrameTime:currentFrameTime]; 112 | 113 | [self updateMeterLabel]; 114 | } 115 | 116 | #pragma mark - 117 | 118 | - (void)start 119 | { 120 | NSURL *tickSoundURL = [[NSBundle bundleForClass:KMCGeigerCounter.class] URLForResource:@"KMCGeigerCounterTick" withExtension:@"aiff"]; 121 | SystemSoundID tickSoundID; 122 | AudioServicesCreateSystemSoundID((__bridge CFURLRef) tickSoundURL, &tickSoundID); 123 | self.tickSoundID = tickSoundID; 124 | 125 | self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkWillDraw:)]; 126 | [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 127 | [self clearLastSecondOfFrameTimes]; 128 | } 129 | 130 | - (void)stop 131 | { 132 | [self.displayLink invalidate]; 133 | self.displayLink = nil; 134 | 135 | AudioServicesDisposeSystemSoundID(self.tickSoundID); 136 | self.tickSoundID = 0; 137 | } 138 | 139 | - (void)setRunning:(BOOL)running 140 | { 141 | if (_running != running) { 142 | if (running) { 143 | [self start]; 144 | } else { 145 | [self stop]; 146 | } 147 | 148 | _running = running; 149 | } 150 | } 151 | 152 | #pragma mark - 153 | 154 | - (void)applicationDidBecomeActive 155 | { 156 | self.running = self.enabled; 157 | } 158 | 159 | - (void)applicationWillResignActive 160 | { 161 | self.running = NO; 162 | } 163 | 164 | #pragma mark - 165 | 166 | - (void)enable 167 | { 168 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 169 | self.window.rootViewController = [[UIViewController alloc] init]; 170 | self.window.windowLevel = self.windowLevel; 171 | self.window.userInteractionEnabled = NO; 172 | 173 | CGFloat const kMeterWidth = 105.0; 174 | CGFloat xOrigin = 0.0; 175 | UIViewAutoresizing autoresizingMask = UIViewAutoresizingFlexibleBottomMargin; 176 | switch (self.position) { 177 | case KMCGeigerCounterPositionLeft: 178 | xOrigin = 0.0; 179 | autoresizingMask |= UIViewAutoresizingFlexibleRightMargin; 180 | break; 181 | case KMCGeigerCounterPositionMiddle: 182 | xOrigin = (CGRectGetWidth(self.window.bounds) - kMeterWidth) / 2.0; 183 | autoresizingMask |= UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin; 184 | break; 185 | case KMCGeigerCounterPositionRight: 186 | xOrigin = (CGRectGetWidth(self.window.bounds) - kMeterWidth); 187 | autoresizingMask |= UIViewAutoresizingFlexibleLeftMargin; 188 | break; 189 | } 190 | 191 | CGFloat meterHeight = fmax(20.0, fmin(30.0, [UIApplication sharedApplication].statusBarFrame.size.height)); 192 | self.meterLabel = [[UILabel alloc] initWithFrame:CGRectMake(xOrigin, 0.0, 193 | kMeterWidth, meterHeight)]; 194 | self.meterLabel.autoresizingMask = autoresizingMask; 195 | self.meterLabel.font = [UIFont boldSystemFontOfSize:12.0]; 196 | self.meterLabel.backgroundColor = [UIColor grayColor]; 197 | self.meterLabel.textColor = [UIColor whiteColor]; 198 | self.meterLabel.textAlignment = NSTextAlignmentCenter; 199 | [self.window.rootViewController.view addSubview:self.meterLabel]; 200 | 201 | self.window.hidden = NO; 202 | 203 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; 204 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil]; 205 | 206 | if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { 207 | self.running = YES; 208 | } 209 | } 210 | 211 | - (void)disable 212 | { 213 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 214 | 215 | self.running = NO; 216 | 217 | self.meterLabel = nil; 218 | self.window = nil; 219 | } 220 | 221 | #pragma mark - Init/dealloc 222 | 223 | - (instancetype)init 224 | { 225 | self = [super init]; 226 | if (self) { 227 | _windowLevel = UIWindowLevelStatusBar + 10.0; 228 | _position = KMCGeigerCounterPositionLeft; 229 | 230 | _meterPerfectColor = [KMCGeigerCounter colorWithHex:0x999999 alpha:1.0]; 231 | _meterGoodColor = [KMCGeigerCounter colorWithHex:0x66a300 alpha:1.0]; 232 | _meterBadColor = [KMCGeigerCounter colorWithHex:0xff7f0d alpha:1.0]; 233 | 234 | if (@available(iOS 10.3, *)) { 235 | _hardwareFramesPerSecond = [UIScreen mainScreen].maximumFramesPerSecond; 236 | } else { 237 | _hardwareFramesPerSecond = 60; 238 | } 239 | 240 | _recentFrameTimes = malloc(sizeof(*_recentFrameTimes) * _hardwareFramesPerSecond); 241 | } 242 | return self; 243 | } 244 | 245 | - (void)dealloc 246 | { 247 | [_displayLink invalidate]; 248 | 249 | if (_tickSoundID) { 250 | AudioServicesDisposeSystemSoundID(_tickSoundID); 251 | } 252 | 253 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 254 | 255 | if (_recentFrameTimes) { 256 | free(_recentFrameTimes); 257 | _recentFrameTimes = nil; 258 | } 259 | } 260 | 261 | #pragma mark - Public interface 262 | 263 | + (instancetype)sharedGeigerCounter 264 | { 265 | static KMCGeigerCounter *instance; 266 | static dispatch_once_t onceToken; 267 | dispatch_once(&onceToken, ^{ 268 | instance = [[KMCGeigerCounter alloc] init]; 269 | }); 270 | return instance; 271 | } 272 | 273 | - (void)setEnabled:(BOOL)enabled 274 | { 275 | if (_enabled != enabled) { 276 | if (enabled) { 277 | [self enable]; 278 | } else { 279 | [self disable]; 280 | } 281 | 282 | _enabled = enabled; 283 | } 284 | } 285 | 286 | - (void)setWindowLevel:(UIWindowLevel)windowLevel 287 | { 288 | _windowLevel = windowLevel; 289 | self.window.windowLevel = windowLevel; 290 | } 291 | 292 | - (NSInteger)droppedFrameCountInLastSecond 293 | { 294 | NSInteger droppedFrameCount = 0; 295 | 296 | CFTimeInterval lastFrameTime = CACurrentMediaTime() - [self hardwareFrameDuration]; 297 | for (NSInteger i = 0; i < self.hardwareFramesPerSecond; ++i) { 298 | if (1.0 <= lastFrameTime - _recentFrameTimes[i]) { 299 | ++droppedFrameCount; 300 | } 301 | } 302 | 303 | return droppedFrameCount; 304 | } 305 | 306 | - (NSInteger)drawnFrameCountInLastSecond 307 | { 308 | if (!self.running || self.frameNumber < self.hardwareFramesPerSecond) { 309 | return -1; 310 | } 311 | 312 | return self.hardwareFramesPerSecond - self.droppedFrameCountInLastSecond; 313 | } 314 | 315 | @end 316 | -------------------------------------------------------------------------------- /KMCGeigerCounter/KMCGeigerCounterTick.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kconner/KMCGeigerCounter/6ff401e9123d392bf1f45e6f916dad4bd811c083/KMCGeigerCounter/KMCGeigerCounterTick.aiff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2014 Kevin Conner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KMCGeigerCounter 2 | 3 | This tool is a framerate meter that clicks like a Geiger counter when your animation drops a frame. 4 | 5 | A Geiger counter detects invisible particles and alerts you to what you can't see. Dropped frames aren't invisible, but it can be hard to tell the difference between 55 and 60 fps. KMCGeigerCounter makes each dropped frame obvious. 6 | 7 | - If you're not consistently animating smoothly, you'll hear a rough, staticky noise. 8 | - If your app runs at a smooth 60 fps, you'll hear the occasional drops to 59 and 58. 9 | - You will hear dropped frames from occasional CPU spikes, like when custom table view cells enter the screen and require layout. 10 | 11 | The meter shows two numbers: 12 | 13 | - The number of frames dropped in the past second 14 | - The number of frames drawn in the past second 15 | 16 | The meter will be orange when you've dropped at least three frames in the past second. 17 | 18 | ## Installation 19 | 20 | `pod 'KMCGeigerCounter'` 21 | 22 | Or copy these files into your project: 23 | 24 | - KMCGeigerCounter.h 25 | - KMCGeigerCounter.m 26 | - KMCGeigerCounter.aiff 27 | 28 | If you're not using CocoaPods, you may need to add this framework to your Link Binary With Libraries build phase: 29 | 30 | - AudioToolbox.framework 31 | 32 | ## Usage 33 | 34 | In your `UIApplicationDelegate`, enable the tool: 35 | 36 | ```objc 37 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 38 | { 39 | // … 40 | [self.window makeKeyAndVisible]; 41 | 42 | [KMCGeigerCounter sharedGeigerCounter].enabled = YES; 43 | } 44 | ``` 45 | 46 | Build and run your app. Navigate through your app and listen for clicks. 47 | 48 | ## Known issue 49 | 50 | Dropped frames on iOS can be divided into two types, which I'll call CPU and GPU drops. CPU drops happen when main thread activity delays the preparation of your layer tree, like when Auto Layout evaluates a complex set of constraints. CPU drops are easy to measure by observing the delivery timing of regularly scheduled events on the main thread. GPU drops happen when the layer tree is expensive to draw, such as when there are too many blended layers. Due to the nature of iOS, GPU drops happen in a system process responsible for drawing. I haven't found a way to measure them without adversely affecting the app's framerate. The upshot is that only CPU drops can be detected by this library today. Fortunately, more powerful iOS devices have made GPU drops much less common than they used to be, and you can always use the Core Animation instrument to measure them faithfully. 51 | 52 | ## Notes 53 | 54 | Remember to turn off Silent mode, or you won't hear anything. 55 | 56 | You should remove KMCGeigerCounter before shipping to the App Store. It can't be good for battery life. 57 | 58 | The iOS Simulator doesn't simulate device performance, so consider enabling the tool only for device builds: 59 | 60 | ```objc 61 | #if !TARGET_IPHONE_SIMULATOR 62 | [KMCGeigerCounter sharedGeigerCounter].enabled = YES; 63 | #endif 64 | ``` 65 | --------------------------------------------------------------------------------