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