├── .arcconfig ├── .arclint ├── .arcunit ├── .clang-format ├── .codecov.yml ├── .gitignore ├── .jazzy.yaml ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MaterialMotionRuntime.podspec ├── Podfile ├── Podfile.lock ├── README.md ├── examples ├── LifeOfAComposablePlanExample.swift ├── LifeOfAConfigurablePlanExample.swift ├── LifeOfAPlanExample.swift ├── TableOfContents.swift ├── TimelineObservationExample.swift └── apps │ └── Catalog │ ├── Catalog.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── Catalog.xcscheme │ │ └── UnitTests.xcscheme │ ├── UnitTests │ └── Info.plist │ └── src │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── main.m ├── src ├── MDMConsoleLoggingTracer.h ├── MDMConsoleLoggingTracer.m ├── MDMMotionRuntime.h ├── MDMMotionRuntime.m ├── MDMPerforming.h ├── MDMPlan.h ├── MDMTimeline.h ├── MDMTimeline.m ├── MDMTracing.h ├── MaterialMotionRuntime.h └── private │ ├── MDMPlanEmitter.h │ ├── MDMPlanEmitter.m │ ├── MDMTargetRegistry.h │ ├── MDMTargetRegistry.m │ ├── MDMTargetScope.h │ ├── MDMTargetScope.m │ ├── MDMToken+Private.h │ ├── MDMToken.h │ ├── MDMToken.m │ ├── MDMTokenPool.h │ └── MDMTokenPool.m └── tests ├── src ├── Emit.swift ├── ExpectableRuntimeDelegate.swift ├── ForeverActive.swift ├── InstantlyInactive.swift ├── RuntimeSpy.swift ├── TimelineSpy.swift └── ViewTargetAltering.swift └── unit ├── CompositionTests.swift ├── ContinuousPerformingTests.swift ├── NamedPlanTests.swift ├── PlanTokenizerTests.swift ├── RuntimeTests.swift └── TimelineTests.swift /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "load": [ 3 | "material-arc-tools/third_party/arc-hook-conphig", 4 | "material-arc-tools/third_party/arc-hook-github-issues", 5 | "material-arc-tools/third_party/arc-jazzy-linter", 6 | "material-arc-tools/third_party/arc-proselint", 7 | "material-arc-tools/third_party/arc-xcode-test-engine", 8 | "material-arc-tools/third_party/clang-format-linter" 9 | ], 10 | "arcanist_configuration": "HookConphig", 11 | "phabricator.uri": "http://codereview.cc/", 12 | "repository.callsign": "MDMRUNTIMEOBJC", 13 | "arc.land.onto.default": "develop", 14 | "arc.feature.start.default": "origin/develop", 15 | "unit.xcode": { 16 | "build": { 17 | "workspace": "MaterialMotionRuntime.xcworkspace", 18 | "scheme": "UnitTests", 19 | "configuration": "Debug", 20 | "destination": "platform=iOS Simulator,name=iPhone 6s" 21 | }, 22 | "coverage": { 23 | "product": "MaterialMotionRuntime.framework/MaterialMotionRuntime" 24 | }, 25 | "pre-build": "pod install" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "chmod": { 4 | "type": "chmod" 5 | }, 6 | "text": { 7 | "type": "text", 8 | "include": "(\\.(m|h|mm|md|swift)$)", 9 | "exclude": [ 10 | "(/Pods/)" 11 | ], 12 | "severity": { 13 | "3": "disabled", 14 | "5": "disabled" 15 | } 16 | }, 17 | "prose": { 18 | "type": "prose", 19 | "include": "(\\.(md)$)", 20 | "exclude": [ 21 | "(^CHANGELOG.md)" 22 | ], 23 | "severity": { 24 | "typography.symbols.curly_quotes": "disabled", 25 | "typography.symbols.ellipsis": "disabled", 26 | "leonard.exclamation.30ppm": "disabled", 27 | "misc.annotations": "warning" 28 | } 29 | }, 30 | "spelling": { 31 | "type": "spelling", 32 | "include": "(\\.(md)$)" 33 | }, 34 | "clang-format": { 35 | "type": "clang-format", 36 | "include": "(\\.(m|h|mm)$)", 37 | "exclude": "(/Pods/)" 38 | }, 39 | "jazzy": { 40 | "type": "jazzy", 41 | "include": "(src/[^\/]+?\\.(h|swift)$)" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.arcunit: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "xcode": { 4 | "type": "xcode-test-engine", 5 | "include": [ 6 | "(\\.(m|h|mm|swift)$)", 7 | "(Podfile)" 8 | ], 9 | "exclude": "(/Pods/)" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | 3 | AllowShortFunctionsOnASingleLine: Inline 4 | AllowShortIfStatementsOnASingleLine: false 5 | AllowShortLoopsOnASingleLine: false 6 | AlwaysBreakBeforeMultilineStrings: false 7 | BinPackParameters: false 8 | ColumnLimit: 0 9 | IndentWrappedFunctionNames: true 10 | ObjCSpaceBeforeProtocolList: true 11 | PointerBindsToType: false 12 | SortIncludes: true 13 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "examples/" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Jazzy 2 | docs/ 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | *.xcworkspace 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: MaterialMotionRuntime 2 | module_version: 6.0.1 3 | umbrella_header: src/MaterialMotionRuntime.h 4 | objc: true 5 | sdk: iphonesimulator 6 | github_url: https://github.com/material-motion/runtime-objc 7 | github_file_prefix: https://github.com/material-motion/runtime-objc/tree/v6.0.1 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8.1 3 | sudo: false 4 | notifications: 5 | email: false 6 | before_install: 7 | - gem install cocoapods --no-rdoc --no-ri --no-document --quiet 8 | - git clone https://github.com/phacility/arcanist.git 9 | - git clone https://github.com/phacility/libphutil.git 10 | - git clone --recursive https://github.com/material-foundation/material-arc-tools.git 11 | - pod install --repo-update 12 | script: 13 | - set -o pipefail 14 | - arcanist/bin/arc unit --everything --trace 15 | - xcodebuild build -workspace MaterialMotionRuntime.xcworkspace -scheme Catalog -sdk "iphonesimulator10.1" -destination "name=iPhone 6s,OS=10.1" ONLY_ACTIVE_ARCH=YES | xcpretty -c; 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Material Motion Runtime for Apple Devices authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history with git log. 6 | 7 | Google Inc. 8 | and other contributors 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement] 8 | (https://cla.developers.google.com/about/google-individual) 9 | (CLA), which you can do online. The CLA is necessary mainly because you own the 10 | copyright to your changes, even after your contribution becomes part of our 11 | codebase, so we need your permission to use and distribute your code. We also 12 | need to be sure of various other things—for instance that you'll tell us if you 13 | know that your code infringes on other people's patents. You don't have to sign 14 | the CLA until after you've submitted your code for review and a member has 15 | approved it, but you must do it before we can put your code into our codebase. 16 | Before you start working on a larger contribution, you should get in touch with 17 | us first through the issue tracker with your idea so that we can help out and 18 | possibly guide you. Coordinating up front makes it much easier to avoid 19 | frustration later on. 20 | 21 | ### Code reviews 22 | 23 | All submissions, including submissions by project members, require review. 24 | We use GitHub pull requests for this purpose. 25 | 26 | ### The small print 27 | 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement] 31 | (https://cla.developers.google.com/about/google-corporate). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MaterialMotionRuntime.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "MaterialMotionRuntime" 3 | s.summary = "Material Motion Runtime for Apple Devices" 4 | s.version = "6.0.1" 5 | s.authors = "The Material Motion Authors" 6 | s.license = "Apache 2.0" 7 | s.homepage = "https://github.com/material-motion/runtime-objc" 8 | s.source = { :git => "https://github.com/material-motion/runtime-objc.git", :tag => "v" + s.version.to_s } 9 | s.platform = :ios, "8.0" 10 | s.requires_arc = true 11 | s.deprecated = true 12 | s.default_subspec = "lib" 13 | 14 | s.subspec "lib" do |ss| 15 | ss.public_header_files = "src/*.h" 16 | ss.source_files = "src/*.{h,m,mm}", "src/private/*.{h,m,mm}" 17 | end 18 | 19 | s.subspec "examples" do |ss| 20 | ss.source_files = "examples/*.{swift}", "examples/supplemental/*.{swift}" 21 | ss.exclude_files = "examples/TableOfContents.swift" 22 | ss.dependency "MaterialMotionRuntime/lib" 23 | end 24 | 25 | s.subspec "tests" do |ss| 26 | ss.source_files = "tests/src/*.{swift}", "tests/src/private/*.{swift}" 27 | ss.dependency "MaterialMotionRuntime/lib" 28 | ss.framework = "XCTest" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | workspace 'MaterialMotionRuntime.xcworkspace' 2 | use_frameworks! 3 | 4 | target "Catalog" do 5 | pod 'CatalogByConvention' 6 | pod 'MaterialMotionRuntime/examples', :path => './' 7 | project 'examples/apps/Catalog/Catalog.xcodeproj' 8 | end 9 | 10 | target "UnitTests" do 11 | project 'examples/apps/Catalog/Catalog.xcodeproj' 12 | pod 'MaterialMotionRuntime/tests', :path => './' 13 | end 14 | 15 | post_install do |installer| 16 | installer.pods_project.targets.each do |target| 17 | target.build_configurations.each do |configuration| 18 | configuration.build_settings['SWIFT_VERSION'] = "3.0" 19 | if target.name.start_with?("Material") 20 | configuration.build_settings['WARNING_CFLAGS'] ="$(inherited) -Wall -Wcast-align -Wconversion -Werror -Wextra -Wimplicit-atomic-properties -Wmissing-prototypes -Wno-sign-conversion -Wno-unused-parameter -Woverlength-strings -Wshadow -Wstrict-selector-match -Wundeclared-selector -Wunreachable-code" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CatalogByConvention (2.0.0) 3 | - MaterialMotionRuntime/examples (6.0.1): 4 | - MaterialMotionRuntime/lib 5 | - MaterialMotionRuntime/lib (6.0.1) 6 | - MaterialMotionRuntime/tests (6.0.1): 7 | - MaterialMotionRuntime/lib 8 | 9 | DEPENDENCIES: 10 | - CatalogByConvention 11 | - MaterialMotionRuntime/examples (from `./`) 12 | - MaterialMotionRuntime/tests (from `./`) 13 | 14 | EXTERNAL SOURCES: 15 | MaterialMotionRuntime: 16 | :path: "./" 17 | 18 | SPEC CHECKSUMS: 19 | CatalogByConvention: be55c2263132e4f9f59299ac8a528ee8715b3275 20 | MaterialMotionRuntime: d3021113b28d332efd0621f66c9e152b3b11e4ff 21 | 22 | PODFILE CHECKSUM: 7a96b6cc54ccc4da1e8e955c12f0b1688026e0e3 23 | 24 | COCOAPODS: 1.1.1 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material Motion Runtime for Apple Devices 2 | 3 | [![Build Status](https://travis-ci.org/material-motion/runtime-objc.svg?branch=develop)](https://travis-ci.org/material-motion/runtime-objc) 4 | [![codecov](https://codecov.io/gh/material-motion/runtime-objc/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/runtime-objc) 5 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/MaterialMotionRuntime.svg)](https://cocoapods.org/pods/MaterialMotionRuntime) 6 | [![Platform](https://img.shields.io/cocoapods/p/MaterialMotionRuntime.svg)](http://cocoadocs.org/docsets/MaterialMotionRuntime) 7 | [![Docs](https://img.shields.io/cocoapods/metrics/doc-percent/MaterialMotionRuntime.svg)](http://cocoadocs.org/docsets/MaterialMotionRuntime) 8 | 9 | The Material Motion Runtime is a tool for describing motion declaratively. 10 | 11 | ## Declarative motion: motion as data 12 | 13 | This library does not do much on its own. What it does do, however, is enable the expression of 14 | motion as discrete units of data that can be introspected, composed, and sent over a wire. 15 | 16 | This library encourages you to describe motion as data, or what we call *plans*. Plans are committed 17 | to a *motion runtime*, or runtime for short. A runtime coordinates the creation of *performers*, 18 | objects responsible for translating plans into concrete execution. 19 | 20 | ## Installation 21 | 22 | ### Installation with CocoaPods 23 | 24 | > CocoaPods is a dependency manager for Objective-C and Swift libraries. CocoaPods automates the 25 | > process of using third-party libraries in your projects. See 26 | > [the Getting Started guide](https://guides.cocoapods.org/using/getting-started.html) for more 27 | > information. You can install it with the following command: 28 | > 29 | > gem install cocoapods 30 | 31 | Add `MaterialMotionRuntime` to your `Podfile`: 32 | 33 | pod 'MaterialMotionRuntime' 34 | 35 | Then run the following command: 36 | 37 | pod install 38 | 39 | ### Usage 40 | 41 | Import the Material Motion Runtime framework: 42 | 43 | @import MaterialMotionRuntime; 44 | 45 | You will now have access to all of the APIs. 46 | 47 | ## Example apps/unit tests 48 | 49 | Check out a local copy of the repo to access the Catalog application by running the following 50 | commands: 51 | 52 | git clone https://github.com/material-motion/runtime-objc.git 53 | cd runtime-objc 54 | pod install 55 | open MaterialMotionRuntime.xcworkspace 56 | 57 | # Guides 58 | 59 | 1. [Architecture](#architecture) 60 | 1. [How to define a new plan and performer type](#how-to-create-a-new-plan-and-performer-type) 61 | 1. [How to commit a plan to a runtime](#how-to-commit-a-plan-to-a-runtime) 62 | 1. [How to commit a named plan to a runtime](#how-to-commit-a-named-plan-to-a-runtime) 63 | 1. [How to handle multiple plan types in Swift](#how-to-handle-multiple-plan-types-in-swift) 64 | 1. [How to configure performers with named plans](#how-to-configure-performers-with-named-plans) 65 | 1. [How to use composition to fulfill plans](#how-to-use-composition-to-fulfill-plans) 66 | 1. [How to indicate continuous performance](#how-to-indicate-continuous-performance) 67 | 1. [How to trace internal runtime events](#how-to-trace-internal-runtime-events) 68 | 1. [How to log runtime events to the console](#how-to-log-runtime-events-to-the-console) 69 | 1. [How to observe timeline events](#how-to-observe-timeline-events) 70 | 71 | ## Architecture 72 | 73 | The Material Motion Runtime consists of two groups of APIs: a runtime/transaction object and a 74 | constellation of protocols loosely consisting of plan and performing types. 75 | 76 | ### MotionRuntime 77 | 78 | The MotionRuntime object is a coordinating entity whose primary responsibility is to fulfill plans 79 | by creating performers. You can create many runtimes throughout the lifetime of your application. A 80 | good rule of thumb is to have one runtime per interaction or transition. 81 | 82 | ### Plan + Performing types 83 | 84 | The Plan and Performing protocol each define the minimal characteristics required for an object to 85 | be considered either a plan or a performer, respectively, by the runtime. 86 | 87 | Plans and performers have a symbiotic relationship. A plan is executed by the performer it defines. 88 | Performer behavior is configured by the provided plan instances. 89 | 90 | Learn more about the Material Motion Runtime by reading the 91 | [Starmap](https://material-motion.github.io/material-motion/starmap/specifications/runtime/). 92 | 93 | ## How to create a new plan and performer type 94 | 95 | The following steps provide copy-pastable snippets of code. 96 | 97 | ### Step 1: Define the plan type 98 | 99 | Questions to ask yourself when creating a new plan type: 100 | 101 | - What do I want my plan/performer to accomplish? 102 | - Will my performer need many plans to achieve the desired outcome? 103 | - How can I name my plan such that it clearly communicates either a **behavior** or a 104 | **change in state**? 105 | 106 | As general rules: 107 | 108 | 1. Plans with an *-able* suffix alter the **behavior** of the target, often indefinitely. Examples: 109 | Draggable, Pinchable, Tossable. 110 | 2. Plans that are *verbs* describe some **change in state**, often over a period of time. Examples: 111 | FadeIn, Tween, SpringTo. 112 | 113 | Code snippets: 114 | 115 | ***In Objective-C:*** 116 | 117 | ```objc 118 | @interface <#Plan#> : NSObject 119 | @end 120 | 121 | @implementation <#Plan#> 122 | @end 123 | ``` 124 | 125 | ***In Swift:*** 126 | 127 | ```swift 128 | class <#Plan#>: NSObject { 129 | } 130 | ``` 131 | 132 | ### Step 2: Define the performer type 133 | 134 | Performers are responsible for fulfilling plans. Fulfillment is possible in a variety of ways: 135 | 136 | - [Performing](https://material-motion.github.io/material-motion-runtime-objc/Protocols/MDMPerforming.html): [How to configure performers with plans](#how-to-configure-performers-with-plans) 137 | - [NamedPlanPerforming](https://material-motion.github.io/material-motion-runtime-objc/Protocols/MDMNamedPlanPerforming.html): [How to configure performers with named plans](#how-to-configure-performers-with-named-plans) 138 | - [ContinuousPerforming](https://material-motion.github.io/material-motion-runtime-objc/Protocols/MDMContinuousPerforming.html): [How to indicate continuous performance](#how-to-indicate-continuous-performance) 139 | - [ComposablePerforming](https://material-motion.github.io/material-motion-runtime-objc/Protocols/MDMComposablePerforming.html): [How to use composition to fulfill plans](#how-to-use-composition-to-fulfill-plans) 140 | 141 | See the associated links for more details on each performing type. 142 | 143 | > Note: only one instance of a type of performer **per target** is ever created. This allows you to 144 | > register multiple plans to the same target in order to configure a performer. See 145 | > [How to configure performers with plans](#how-to-configure-performers-with-plans) for more details. 146 | 147 | Code snippets: 148 | 149 | ***In Objective-C:*** 150 | 151 | ```objc 152 | @interface <#Performer#> : NSObject 153 | @end 154 | 155 | @implementation <#Performer#> { 156 | UIView *_target; 157 | } 158 | 159 | - (instancetype)initWithTarget:(id)target { 160 | self = [super init]; 161 | if (self) { 162 | assert([target isKindOfClass:[UIView class]]); 163 | _target = target; 164 | } 165 | return self; 166 | } 167 | 168 | - (void)addPlan:(id)plan { 169 | <#Plan#>* <#casted plan instance#> = plan; 170 | 171 | // Do something with the plan. 172 | } 173 | 174 | @end 175 | ``` 176 | 177 | ***In Swift:*** 178 | 179 | ```swift 180 | class <#Performer#>: NSObject, Performing { 181 | let target: UIView 182 | required init(target: Any) { 183 | self.target = target as! UIView 184 | super.init() 185 | } 186 | 187 | func addPlan(_ plan: Plan) { 188 | let <#casted plan instance#> = plan as! <#Plan#> 189 | 190 | // Do something with the plan. 191 | } 192 | } 193 | ``` 194 | 195 | ### Step 3: Make the plan type a formal Plan 196 | 197 | Conforming to Plan requires: 198 | 199 | 1. that you define the type of performer your plan requires, and 200 | 2. that your plan be copyable. 201 | 202 | Code snippets: 203 | 204 | ***In Objective-C:*** 205 | 206 | ```objc 207 | @interface <#Plan#> : NSObject 208 | @end 209 | 210 | @implementation <#Plan#> 211 | 212 | - (Class)performerClass { 213 | return [<#Plan#> class]; 214 | } 215 | 216 | - (id)copyWithZone:(NSZone *)zone { 217 | return [[[self class] allocWithZone:zone] init]; 218 | } 219 | 220 | @end 221 | ``` 222 | 223 | ***In Swift:*** 224 | 225 | ```swift 226 | class <#Plan#>: NSObject, Plan { 227 | func performerClass() -> AnyClass { 228 | return <#Performer#>.self 229 | } 230 | func copy(with zone: NSZone? = nil) -> Any { 231 | return <#Plan#>() 232 | } 233 | } 234 | ``` 235 | 236 | ## How to commit a plan to a runtime 237 | 238 | ### Step 1: Create and store a reference to a runtime instance 239 | 240 | Code snippets: 241 | 242 | ***In Objective-C:*** 243 | 244 | ```objc 245 | @interface MyClass () 246 | @property(nonatomic, strong) MDMMotionRuntime* runtime; 247 | @end 248 | 249 | - (instancetype)init... { 250 | ... 251 | self.runtime = [MDMMotionRuntime new]; 252 | ... 253 | } 254 | ``` 255 | 256 | ***In Swift:*** 257 | 258 | ```swift 259 | class MyClass { 260 | let runtime = MotionRuntime() 261 | } 262 | ``` 263 | 264 | ### Step 2: Associate plans with targets 265 | 266 | Code snippets: 267 | 268 | ***In Objective-C:*** 269 | 270 | ```objc 271 | [runtime addPlan:<#Plan instance#> to:<#View instance#>]; 272 | ``` 273 | 274 | ***In Swift:*** 275 | 276 | ```swift 277 | runtime.addPlan(<#Plan instance#>, to:<#View instance#>) 278 | ``` 279 | 280 | ## How to commit a named plan to a runtime 281 | 282 | ### Step 1: Create and store a reference to a runtime instance 283 | 284 | Code snippets: 285 | 286 | ***In Objective-C:*** 287 | 288 | ```objc 289 | @interface MyClass () 290 | @property(nonatomic, strong) MDMMotionRuntime* runtime; 291 | @end 292 | 293 | - (instancetype)init... { 294 | ... 295 | self.runtime = [MDMMotionRuntime new]; 296 | ... 297 | } 298 | ``` 299 | 300 | ***In Swift:*** 301 | 302 | ```swift 303 | class MyClass { 304 | let runtime = MotionRuntime() 305 | } 306 | ``` 307 | 308 | ### Step 2: Associate named plans with targets 309 | 310 | Code snippets: 311 | 312 | ***In Objective-C:*** 313 | 314 | ```objc 315 | [runtime addPlan:<#Plan instance#> named:<#name#> to:<#View instance#>]; 316 | ``` 317 | 318 | ***In Swift:*** 319 | 320 | ```swift 321 | runtime.addPlan(<#Plan instance#>, named:<#name#>, to:<#View instance#>) 322 | ``` 323 | 324 | ## How to handle multiple plan types in Swift 325 | 326 | Make use of Swift's typed switch/casing to handle multiple plan types. 327 | 328 | ```swift 329 | func addPlan(_ plan: Plan) { 330 | switch plan { 331 | case let <#plan instance 1#> as <#Plan type 1#>: 332 | () 333 | 334 | case let <#plan instance 2#> as <#Plan type 2#>: 335 | () 336 | 337 | case is <#Plan type 3#>: 338 | () 339 | 340 | default: 341 | assert(false) 342 | } 343 | } 344 | ``` 345 | 346 | ## How to configure performers with named plans 347 | 348 | Code snippets: 349 | 350 | ***In Objective-C:*** 351 | 352 | ```objc 353 | @interface <#Performer#> (NamedPlanPerforming) 354 | @end 355 | 356 | @implementation <#Performer#> (NamedPlanPerforming) 357 | 358 | - (void)addPlan:(id)plan named:(NSString *)name { 359 | <#Plan#>* <#casted plan instance#> = plan; 360 | 361 | // Do something with the plan. 362 | } 363 | 364 | - (void)removePlanNamed:(NSString *)name { 365 | // Remove any configuration associated with the given name. 366 | } 367 | 368 | @end 369 | ``` 370 | 371 | ***In Swift:*** 372 | 373 | ```swift 374 | extension <#Performer#>: NamedPlanPerforming { 375 | func addPlan(_ plan: NamedPlan, named name: String) { 376 | let <#casted plan instance#> = plan as! <#Plan#> 377 | 378 | // Do something with the plan. 379 | } 380 | 381 | func removePlan(named name: String) { 382 | // Remove any configuration associated with the given name. 383 | } 384 | } 385 | ``` 386 | 387 | ## How to use composition to fulfill plans 388 | 389 | A composition performer is able to emit new plans using a plan emitter. This feature enables the 390 | reuse of plans and the creation of higher-order abstractions. 391 | 392 | ### Step 1: Conform to ComposablePerforming and store the plan emitter 393 | 394 | Code snippets: 395 | 396 | ***In Objective-C:*** 397 | 398 | ```objc 399 | @interface <#Performer#> () 400 | @property(nonatomic, strong) id planEmitter; 401 | @end 402 | 403 | @interface <#Performer#> (Composition) 404 | @end 405 | 406 | @implementation <#Performer#> (Composition) 407 | 408 | - (void)setPlanEmitter:(id)planEmitter { 409 | self.planEmitter = planEmitter; 410 | } 411 | 412 | @end 413 | ``` 414 | 415 | ***In Swift:*** 416 | 417 | ```swift 418 | // Store the emitter in your class' definition. 419 | class <#Performer#>: ... { 420 | ... 421 | var emitter: PlanEmitting! 422 | ... 423 | } 424 | 425 | extension <#Performer#>: ComposablePerforming { 426 | var emitter: PlanEmitting! 427 | func setPlanEmitter(_ planEmitter: PlanEmitting) { 428 | emitter = planEmitter 429 | } 430 | } 431 | ``` 432 | 433 | ### Step 2: Emit plans 434 | 435 | Performers are only able to emit plans for their associated target. 436 | 437 | Code snippets: 438 | 439 | ***In Objective-C:*** 440 | 441 | ```objc 442 | [self.planEmitter emitPlan:<#(nonnull id)#>]; 443 | ``` 444 | 445 | ***In Swift:*** 446 | 447 | ```swift 448 | emitter.emitPlan<#T##Plan#>) 449 | ``` 450 | 451 | ## How to indicate continuous performance 452 | 453 | Performers will often perform their actions over a period of time or while an interaction is 454 | active. These types of performers are called continuous performers. 455 | 456 | A continuous performer is able to affect the active state of the runtime by generating activity 457 | tokens. The runtime is considered active so long as an activity token is active. Continuous 458 | performers are expected to activate and deactivate tokens when ongoing work starts and finishes, 459 | respectively. 460 | 461 | For example, a performer that registers a platform animation might activate a token when the 462 | animation starts. When the animation completes the token would be deactivated. 463 | 464 | ### Step 1: Conform to ContinuousPerforming and store the token generator 465 | 466 | Code snippets: 467 | 468 | ***In Objective-C:*** 469 | 470 | ```objc 471 | @interface <#Performer#> () 472 | @property(nonatomic, strong) id tokenizer; 473 | @end 474 | 475 | @interface <#Performer#> (Composition) 476 | @end 477 | 478 | @implementation <#Performer#> (Composition) 479 | 480 | - (void)givePlanTokenizer:(id)tokenizer { 481 | self.tokenizer = tokenizer; 482 | } 483 | 484 | @end 485 | ``` 486 | 487 | ***In Swift:*** 488 | 489 | ```swift 490 | // Store the emitter in your class' definition. 491 | class <#Performer#>: ... { 492 | ... 493 | var tokenizer: PlanTokenizing! 494 | ... 495 | } 496 | 497 | extension <#Performer#>: ContinuousPerforming { 498 | func givePlanTokenizer(_ tokenizer: PlanTokenizing) { 499 | self.tokenizer = tokenizer 500 | } 501 | } 502 | ``` 503 | 504 | ### Step 2: Generate a token 505 | 506 | If your work completes in a callback then you will likely need to store the token in order to be 507 | able to reference it at a later point. 508 | 509 | Code snippets: 510 | 511 | ***In Objective-C:*** 512 | 513 | ```objc 514 | id token = [self.tokenizer tokenForPlan:<#plan#>]; 515 | tokenMap[animation] = token; 516 | ``` 517 | 518 | ***In Swift:*** 519 | 520 | ```swift 521 | let token = tokenizer.generate(for: <#plan#>)! 522 | tokenMap[animation] = token 523 | ``` 524 | 525 | ### Step 3: Activate the token when work begins 526 | 527 | Code snippets: 528 | 529 | ***In Objective-C:*** 530 | 531 | ```objc 532 | id token = tokenMap[animation]; 533 | token.active = true; 534 | ``` 535 | 536 | ***In Swift:*** 537 | 538 | ```swift 539 | tokenMap[animation].isActive = true 540 | ``` 541 | 542 | ### Step 4: Deactivate the token when work has completed 543 | 544 | Code snippets: 545 | 546 | ***In Objective-C:*** 547 | 548 | ```objc 549 | id token = tokenMap[animation]; 550 | token.active = false; 551 | ``` 552 | 553 | ***In Swift:*** 554 | 555 | ```swift 556 | tokenMap[animation].isActive = false 557 | ``` 558 | 559 | ## How to trace internal runtime events 560 | 561 | Tracing allows you to observe internal events occurring within a runtime. This information may be 562 | used for the following purposes: 563 | 564 | - Debug logging. 565 | - Inspection tooling. 566 | 567 | Use for other purposes is unsupported. 568 | 569 | ### Step 1: Create a tracer class 570 | 571 | Code snippets: 572 | 573 | ***In Objective-C:*** 574 | 575 | ```objc 576 | @interface <#Custom tracer#> : NSObject 577 | @end 578 | 579 | @implementation <#Custom tracer#> 580 | @end 581 | ``` 582 | 583 | ***In Swift:*** 584 | 585 | ```swift 586 | class <#Custom tracer#>: NSObject, Tracing { 587 | } 588 | ``` 589 | 590 | ### Step 2: Implement methods 591 | 592 | The documentation for the Tracing protocol enumerates the available methods. 593 | 594 | Code snippets: 595 | 596 | ***In Objective-C:*** 597 | 598 | ```objc 599 | @implementation <#Custom tracer#> 600 | 601 | - (void)didAddPlan:(id)plan to:(id)target { 602 | 603 | } 604 | 605 | @end 606 | ``` 607 | 608 | ***In Swift:*** 609 | 610 | ```swift 611 | class <#Custom tracer#>: NSObject, Tracing { 612 | func didAddPlan(_ plan: Plan, to target: Any) { 613 | 614 | } 615 | } 616 | ``` 617 | 618 | ## How to log runtime events to the console 619 | 620 | Code snippets: 621 | 622 | ***In Objective-C:*** 623 | 624 | ```objc 625 | [runtime addTracer:[MDMConsoleLoggingTracer new]]; 626 | ``` 627 | 628 | ***In Swift:*** 629 | 630 | ```swift 631 | runtime.addTracer(ConsoleLoggingTracer()) 632 | ``` 633 | 634 | ## How to observe timeline events 635 | 636 | ### Step 1: Conform to the TimelineObserving protocol 637 | 638 | Code snippets: 639 | 640 | ***In Objective-C:*** 641 | 642 | ```objc 643 | @interface <#SomeClass#> () 644 | @end 645 | 646 | @implementation <#SomeClass#> 647 | 648 | - (void)timeline:(MDMTimeline *)timeline didAttachScrubber:(MDMTimelineScrubber *)scrubber { 649 | 650 | } 651 | 652 | - (void)timeline:(MDMTimeline *)timeline didDetachScrubber:(MDMTimelineScrubber *)scrubber { 653 | 654 | } 655 | 656 | - (void)timeline:(MDMTimeline *)timeline scrubberDidScrub:(NSTimeInterval)timeOffset { 657 | 658 | } 659 | 660 | @end 661 | ``` 662 | 663 | ***In Swift:*** 664 | 665 | ```swift 666 | extension <#SomeClass#>: TimelineObserving { 667 | func timeline(_ timeline: Timeline, didAttach scrubber: TimelineScrubber) { 668 | } 669 | 670 | func timeline(_ timeline: Timeline, didDetach scrubber: TimelineScrubber) { 671 | } 672 | 673 | func timeline(_ timeline: Timeline, scrubberDidScrub timeOffset: TimeInterval) { 674 | } 675 | } 676 | ``` 677 | 678 | ### Step 2: Add your observer to a timeline 679 | 680 | Code snippets: 681 | 682 | ***In Objective-C:*** 683 | 684 | ```objc 685 | [timeline addTimelineObserver:<#(nonnull id)#>]; 686 | ``` 687 | 688 | ***In Swift:*** 689 | 690 | ```swift 691 | timeline.addObserver(<#T##observer: TimelineObserving##TimelineObserving#>) 692 | ``` 693 | 694 | ## Contributing 695 | 696 | We welcome contributions! 697 | 698 | Check out our [upcoming milestones](https://github.com/material-motion/runtime-objc/milestones). 699 | 700 | Learn more about [our team](https://material-motion.github.io/material-motion/team/), 701 | [our community](https://material-motion.github.io/material-motion/team/community/), and 702 | our [contributor essentials](https://material-motion.github.io/material-motion/team/essentials/). 703 | 704 | ## License 705 | 706 | Licensed under the Apache 2.0 license. See LICENSE for details. 707 | 708 | -------------------------------------------------------------------------------- /examples/LifeOfAComposablePlanExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MaterialMotionRuntime 19 | 20 | // This example demonstrates how to use composition to create a complex interaction composed of 21 | // many plans. Building off of our draggable examples, we'll be making the view in this example 22 | // tossable. The user can drag anywhere on the screen to grab the square. The square can then be 23 | // tossed in any direction and it will spring back to the center of the screen. 24 | public class LifeOfAComposablePlanExampleController: UIViewController { 25 | let runtime = MotionRuntime() 26 | 27 | func commonInit() { 28 | title = "Touch anywhere to toss the square" 29 | } 30 | 31 | // MARK: Configuring views and interactions 32 | 33 | override public func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | view.backgroundColor = .white 37 | 38 | let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) 39 | square.backgroundColor = .red 40 | view.addSubview(square) 41 | 42 | let pan = UIPanGestureRecognizer() 43 | view.addGestureRecognizer(pan) 44 | 45 | // Notice that our view controller is only concerned with one plan: Tossable. This plan's 46 | // performer will coordinate the emission of plans in reaction to the gesture recognizer's 47 | // events. 48 | runtime.addPlan(Tossable(gestureRecognizer: pan), to: square) 49 | } 50 | 51 | // MARK: Routing initializers 52 | 53 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 54 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 55 | 56 | commonInit() 57 | } 58 | 59 | required public init?(coder aDecoder: NSCoder) { 60 | super.init(coder: aDecoder) 61 | 62 | commonInit() 63 | } 64 | } 65 | 66 | // MARK: - Composite plans 67 | 68 | // Enables the target to be dragged and, upon release, to be tossed to the midpoint of the target's 69 | // parent view using UIDynamics. 70 | private class Tossable: NSObject, Plan { 71 | let gestureRecognizer: UIPanGestureRecognizer 72 | required init(gestureRecognizer: UIPanGestureRecognizer) { 73 | self.gestureRecognizer = gestureRecognizer 74 | super.init() 75 | } 76 | 77 | func performerClass() -> AnyClass { 78 | return Performer.self 79 | } 80 | 81 | func copy(with zone: NSZone? = nil) -> Any { 82 | return Tossable(gestureRecognizer: gestureRecognizer) 83 | } 84 | 85 | private class Performer: NSObject, ComposablePerforming { 86 | let target: UIView 87 | required init(target: Any) { 88 | self.target = target as! UIView 89 | super.init() 90 | } 91 | 92 | func addPlan(_ plan: Plan) { 93 | let tossable = plan as! Tossable 94 | 95 | // Draggable is being reused from the Life of a Configurable Plan example. 96 | emitter.emitPlan(Draggable(panGestureRecognizer: tossable.gestureRecognizer)) 97 | 98 | tossable.gestureRecognizer.addTarget(self, action: #selector(didPan(gesture:))) 99 | } 100 | 101 | func didPan(gesture: UIPanGestureRecognizer) { 102 | switch gesture.state { 103 | case .began: 104 | emitter.emitPlan(Grabbed()) 105 | 106 | case .ended: fallthrough 107 | case .cancelled: 108 | let midpoint = CGPoint(x: target.superview!.bounds.midX, 109 | y: target.superview!.bounds.midY) 110 | emitter.emitPlan(Anchored(to: midpoint)) 111 | emitter.emitPlan(Impulse(velocity: gesture.velocity(in: target))) 112 | 113 | default: () 114 | } 115 | } 116 | 117 | var emitter: PlanEmitting! 118 | func setPlanEmitter(_ planEmitter: PlanEmitting) { 119 | emitter = planEmitter 120 | } 121 | } 122 | } 123 | 124 | // MARK: - UIDynamics plans 125 | 126 | // Anchors the target to a given position using UIDynamics. 127 | private class Anchored: NSObject { 128 | let position: CGPoint 129 | init(to position: CGPoint) { 130 | self.position = position 131 | super.init() 132 | } 133 | } 134 | 135 | // Applies an instantaneous impulse to the target using UIDynamics. 136 | private class Impulse: NSObject { 137 | let velocity: CGPoint 138 | init(velocity: CGPoint) { 139 | self.velocity = velocity 140 | super.init() 141 | } 142 | } 143 | 144 | // Removes all active UIDynamics behaviors associated with the target. 145 | private class Grabbed: NSObject {} 146 | 147 | private class UIDynamicsPerformer: NSObject, Performing { 148 | let target: UIView 149 | let dynamicAnimator: UIDynamicAnimator 150 | required init(target: Any) { 151 | self.target = target as! UIView 152 | dynamicAnimator = UIDynamicAnimator(referenceView: self.target.superview!) 153 | super.init() 154 | } 155 | 156 | var snapBehavior: UISnapBehavior? 157 | func addPlan(_ plan: Plan) { 158 | switch plan { 159 | case let anchoredTo as Anchored: 160 | if let behavior = snapBehavior { 161 | dynamicAnimator.removeBehavior(behavior) 162 | } 163 | snapBehavior = UISnapBehavior(item: target, snapTo: anchoredTo.position) 164 | dynamicAnimator.addBehavior(snapBehavior!) 165 | 166 | case let impulse as Impulse: 167 | let push = UIPushBehavior(items: [target], mode: .instantaneous) 168 | let velocity = impulse.velocity 169 | 170 | let direction = CGVector(dx: velocity.x / 100, dy: velocity.y / 100) 171 | push.pushDirection = direction 172 | dynamicAnimator.addBehavior(push) 173 | 174 | case is Grabbed: 175 | dynamicAnimator.removeAllBehaviors() 176 | snapBehavior = nil 177 | 178 | default: 179 | assert(false) 180 | } 181 | } 182 | } 183 | 184 | // MARK: Plan conformity 185 | 186 | // Note that we're using extensions to implement Plan conformity in this example so that the code 187 | // above can focus on the novel bits. 188 | 189 | extension Anchored: Plan { 190 | func performerClass() -> AnyClass { 191 | return UIDynamicsPerformer.self 192 | } 193 | 194 | func copy(with zone: NSZone? = nil) -> Any { 195 | return Anchored(to: position) 196 | } 197 | } 198 | 199 | extension Grabbed: Plan { 200 | func performerClass() -> AnyClass { 201 | return UIDynamicsPerformer.self 202 | } 203 | 204 | func copy(with zone: NSZone? = nil) -> Any { 205 | return Grabbed() 206 | } 207 | } 208 | 209 | extension Impulse: Plan { 210 | func performerClass() -> AnyClass { 211 | return UIDynamicsPerformer.self 212 | } 213 | 214 | func copy(with zone: NSZone? = nil) -> Any { 215 | return Impulse(velocity: velocity) 216 | } 217 | } 218 | 219 | private func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 220 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 221 | } 222 | -------------------------------------------------------------------------------- /examples/LifeOfAConfigurablePlanExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MaterialMotionRuntime 19 | 20 | // This example demonstrates how to configure a performer using properties on a plan. We recreate 21 | // the Draggable plan found in Life of a Plan, but this time with a configurable 22 | // panGestureRecognizer property. We assign a pre-made pan gesture recognizer that's already 23 | // associated with the view controller's root view, allowing us to drag the square by touching 24 | // anywhere in the view controller. 25 | public class LifeOfAConfigurablePlanViewController: UIViewController { 26 | let runtime = MotionRuntime() 27 | 28 | func commonInit() { 29 | self.title = "Touch anywhere to drag the square" 30 | } 31 | 32 | // MARK: Configuring views and interactions 33 | 34 | override public func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | view.backgroundColor = .white 38 | 39 | let squareView = UIView(frame: CGRect(x: 100, y: 200, width: 100, height: 100)) 40 | squareView.backgroundColor = .red 41 | view.addSubview(squareView) 42 | 43 | // Note that we're adding the pan gesture recognizer to our view controller's root view and 44 | // providing the gesture recognizer to our plan. 45 | let panGestureRecognizer = UIPanGestureRecognizer() 46 | view.addGestureRecognizer(panGestureRecognizer) 47 | 48 | runtime.addPlan(Draggable(panGestureRecognizer: panGestureRecognizer), to: squareView) 49 | } 50 | 51 | // MARK: Routing initializers 52 | 53 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 54 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 55 | 56 | self.commonInit() 57 | } 58 | 59 | required public init?(coder aDecoder: NSCoder) { 60 | super.init(coder: aDecoder) 61 | 62 | self.commonInit() 63 | } 64 | } 65 | 66 | class Draggable: NSObject, Plan { 67 | var panGestureRecognizer: UIPanGestureRecognizer? 68 | 69 | init(panGestureRecognizer: UIPanGestureRecognizer) { 70 | self.panGestureRecognizer = panGestureRecognizer 71 | 72 | super.init() 73 | } 74 | override init() { 75 | super.init() 76 | } 77 | 78 | func performerClass() -> AnyClass { 79 | return Performer.self 80 | } 81 | func copy(with zone: NSZone? = nil) -> Any { 82 | let copy = Draggable() 83 | copy.panGestureRecognizer = panGestureRecognizer 84 | return copy 85 | } 86 | 87 | // Our performer now conforms to Performing. This allows our performer to receive plan 88 | // instances as they are committed. 89 | private class Performer: NSObject, Performing { 90 | let target: UIView 91 | required init(target: Any) { 92 | self.target = target as! UIView 93 | super.init() 94 | } 95 | 96 | func addPlan(_ plan: Plan) { 97 | // We must downcast our plan to the expected type in order to access its properties. We use 98 | // ! to enforce this expectation at runtime. 99 | let draggable = plan as! Draggable 100 | 101 | let selector = #selector(didPan(gestureRecognizer:)) 102 | 103 | if let panGestureRecognizer = draggable.panGestureRecognizer { 104 | // Listen to this gesture recognizer's events. 105 | panGestureRecognizer.addTarget(self, action: selector) 106 | } else { 107 | // Create a gesture recognizer and associate it with the target. 108 | let gestureRecognizer = UIPanGestureRecognizer(target: self, action: selector) 109 | self.target.addGestureRecognizer(gestureRecognizer) 110 | } 111 | } 112 | 113 | var lastTranslation: CGPoint = .zero 114 | func didPan(gestureRecognizer: UIPanGestureRecognizer) { 115 | let translation = gestureRecognizer.translation(in: target) 116 | let isActive = gestureRecognizer.state == .began || gestureRecognizer.state == .changed 117 | if isActive { 118 | let delta = CGPoint(x: translation.x - lastTranslation.x, 119 | y: translation.y - lastTranslation.y) 120 | target.center = target.center + delta 121 | lastTranslation = translation 122 | } else { 123 | lastTranslation = .zero 124 | } 125 | } 126 | } 127 | } 128 | 129 | private func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 130 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 131 | } 132 | -------------------------------------------------------------------------------- /examples/LifeOfAPlanExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MaterialMotionRuntime 19 | 20 | // This example demonstrates the development a new plan/performer pair and the committment of the 21 | // plan to a runtime. We create a "Draggable" plan that enables its associated view to be dragged. 22 | public class LifeOfAPlanViewController: UIViewController { 23 | 24 | func commonInit() { 25 | self.title = "Touch the square to drag it" 26 | } 27 | 28 | // Let's define a new object, Draggable, that is a type of Plan. Plans must implement two methods: 29 | // 30 | // - performerClass(), and 31 | // - copy(zone:). 32 | // 33 | // performerClass() defines which performer class should be instantiated in order to fulfill the 34 | // plan. 35 | // 36 | // copy(zone:) is required by the NSCopying protocol. It is required because plans are copied when 37 | // they're added to a runtime. 38 | private class Draggable: NSObject, Plan { 39 | func performerClass() -> AnyClass { 40 | return Performer.self 41 | } 42 | func copy(with zone: NSZone? = nil) -> Any { 43 | return Draggable() 44 | } 45 | 46 | // App code should only ever think in terms of Plan types, so we've made our Performer type a 47 | // private implementation detail. 48 | private class Performer: NSObject, Performing { 49 | let gestureRecognizer: UIPanGestureRecognizer 50 | 51 | // Performers must implement the init(target:) initializer. The target is the object to which 52 | // the plan was associated and to which the performer should apply modifications. 53 | let target: UIView 54 | required init(target: Any) { 55 | self.target = target as! UIView 56 | gestureRecognizer = UIPanGestureRecognizer() 57 | 58 | super.init() 59 | 60 | gestureRecognizer.addTarget(self, action: #selector(didPan(gestureRecognizer:))) 61 | 62 | // For this example our performer adds a gesture recognizer to the view. This has the 63 | // advantage of simplifying our view controller code; associate a Draggable plan with our 64 | // view and we're done. 65 | // 66 | // Some questions to ask: 67 | // 68 | // - What if the view already has a gesture recognizer that we'd like to use? 69 | // - What if we'd like to register the gesture recognizer to a different view? 70 | // 71 | // We explore answers to these questions in LifeOfAConfigurablePlan. 72 | self.target.addGestureRecognizer(gestureRecognizer) 73 | } 74 | 75 | public func addPlan(_ plan: Plan) { 76 | // We don't use the plan object in this example because everything is configured in the 77 | // initializer. 78 | } 79 | 80 | // Extract translation values from the pan gesture recognizer and add them to the view's center 81 | // point. 82 | var lastTranslation: CGPoint = .zero 83 | func didPan(gestureRecognizer: UIPanGestureRecognizer) { 84 | let translation = gestureRecognizer.translation(in: target) 85 | let isActive = gestureRecognizer.state == .began || gestureRecognizer.state == .changed 86 | if isActive { 87 | let delta = CGPoint(x: translation.x - lastTranslation.x, 88 | y: translation.y - lastTranslation.y) 89 | target.center = target.center + delta 90 | lastTranslation = translation 91 | } else { 92 | lastTranslation = .zero 93 | } 94 | } 95 | } 96 | } 97 | 98 | // MARK: Configuring views and interactions 99 | 100 | // We create a single MotionRuntime for the lifetime of this view controller. How many runtimes you 101 | // decide to create is a matter of preference, but generally speaking it's fair to create one 102 | // runtime per self-contained interaction or transition. 103 | let runtime = MotionRuntime() 104 | 105 | override public func viewDidLoad() { 106 | super.viewDidLoad() 107 | 108 | view.backgroundColor = .white 109 | 110 | let squareView = UIView(frame: CGRect(x: 100, y: 200, width: 100, height: 100)) 111 | squareView.backgroundColor = .red 112 | view.addSubview(squareView) 113 | 114 | // Associate a Draggable plan with squareView. 115 | runtime.addPlan(Draggable(), to: squareView) 116 | } 117 | 118 | // MARK: Routing initializers 119 | 120 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 121 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 122 | 123 | self.commonInit() 124 | } 125 | 126 | required public init?(coder aDecoder: NSCoder) { 127 | super.init(coder: aDecoder) 128 | 129 | self.commonInit() 130 | } 131 | } 132 | 133 | // Enables `target.center + delta` 134 | private func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 135 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 136 | } 137 | -------------------------------------------------------------------------------- /examples/TableOfContents.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // MARK: Catalog by convention 18 | 19 | import MaterialMotionRuntime 20 | 21 | extension LifeOfAPlanViewController { 22 | class func catalogBreadcrumbs() -> [String] { 23 | return ["1. Life of a plan"] 24 | } 25 | } 26 | 27 | extension LifeOfAConfigurablePlanViewController { 28 | class func catalogBreadcrumbs() -> [String] { 29 | return ["2. Life of a configurable plan"] 30 | } 31 | } 32 | 33 | extension LifeOfAComposablePlanExampleController { 34 | class func catalogBreadcrumbs() -> [String] { 35 | return ["3. Life of a composable plan"] 36 | } 37 | } 38 | 39 | extension TimelineObservationExampleViewController { 40 | class func catalogBreadcrumbs() -> [String] { 41 | return ["4. Timeline observation"] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/TimelineObservationExample.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import MaterialMotionRuntime 19 | 20 | // This example demonstrates how to create a Timeline and observe changes to its state by 21 | // implementing the TimelineObserving protocol. We create a rudimentary API to mimic the ability to 22 | // attach/detach a scrubber and to be able to change its timeOffset. 23 | public class TimelineObservationExampleViewController: UIViewController, TimelineObserving { 24 | 25 | let timeline = Timeline() 26 | let maximumTimeOffset: Float = 10 27 | 28 | func commonInit() { 29 | self.title = "Enable the slider to scrub the timeline" 30 | 31 | timeline.addObserver(self) 32 | } 33 | 34 | // MARK: Configuring views and interactions 35 | 36 | var slider: UISlider! 37 | 38 | override public func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | view.backgroundColor = .white 42 | 43 | let toggle = UISwitch() 44 | let toggleSize = toggle.sizeThatFits(view.bounds.size) 45 | toggle.frame = CGRect(x: view.bounds.width / 2 - toggleSize.width / 2, 46 | y: 100, 47 | width: toggleSize.width, 48 | height: toggleSize.height) 49 | toggle.addTarget(self, action: #selector(toggleDidChange(_:)), for: .valueChanged) 50 | view.addSubview(toggle) 51 | 52 | slider = UISlider() 53 | let sliderSize = slider.sizeThatFits(view.bounds.size) 54 | slider.frame = CGRect(x: 24, 55 | y: view.bounds.height - sliderSize.height - 24, 56 | width: view.bounds.width - 48, 57 | height: sliderSize.height) 58 | slider.addTarget(self, action: #selector(sliderDidChange(_:)), for: .valueChanged) 59 | slider.maximumValue = maximumTimeOffset 60 | slider.isEnabled = false 61 | view.addSubview(slider) 62 | } 63 | 64 | func toggleDidChange(_ toggle: UISwitch) { 65 | slider.isEnabled = toggle.isOn 66 | if toggle.isOn { 67 | timeline.attachScrubber(withTimeOffset: TimeInterval(slider.value)) 68 | } else { 69 | timeline.detachScrubber() 70 | } 71 | } 72 | 73 | func sliderDidChange(_ slider: UISlider) { 74 | timeline.scrubber?.timeOffset = TimeInterval(slider.value) 75 | } 76 | 77 | // MARK: TimelineObserving 78 | 79 | public func timeline(_ timeline: Timeline, scrubberDidScrub timeOffset: TimeInterval) { 80 | print("Scrubber did scrub: \(timeOffset)") 81 | } 82 | 83 | public func timeline(_ timeline: Timeline, didAttach scrubber: TimelineScrubber) { 84 | print("Did attach scrubber") 85 | dump(scrubber) 86 | } 87 | 88 | public func timeline(_ timeline: Timeline, didDetach scrubber: TimelineScrubber) { 89 | print("Did detach scrubber") 90 | dump(scrubber) 91 | } 92 | 93 | // MARK: Routing initializers 94 | 95 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 96 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 97 | 98 | self.commonInit() 99 | } 100 | 101 | required public init?(coder aDecoder: NSCoder) { 102 | super.init(coder: aDecoder) 103 | 104 | self.commonInit() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 12D431C606F2CA5213DFE9E2 /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 461501D72C29FCDB7E2578A1 /* Pods_UnitTests.framework */; }; 11 | 661369711D5BE7CB00F6830F /* CompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661369701D5BE7CB00F6830F /* CompositionTests.swift */; }; 12 | 661CD8091DE4E97200841D14 /* NamedPlanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661CD8081DE4E97200841D14 /* NamedPlanTests.swift */; }; 13 | 6624C6431D466D7C00DF3108 /* ContinuousPerformingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6624C6421D466D7C00DF3108 /* ContinuousPerformingTests.swift */; }; 14 | 66876C601D25ED610054CED4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 66876C5F1D25ED610054CED4 /* main.m */; }; 15 | 66876C631D25ED610054CED4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 66876C621D25ED610054CED4 /* AppDelegate.m */; }; 16 | 66876C6B1D25ED610054CED4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66876C6A1D25ED610054CED4 /* Assets.xcassets */; }; 17 | 66876C6E1D25ED610054CED4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 66876C6C1D25ED610054CED4 /* LaunchScreen.storyboard */; }; 18 | 669256F21DD6134C00F659E6 /* TimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669256F11DD6134C00F659E6 /* TimelineTests.swift */; }; 19 | 669F4A341D86073A00B0C45C /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F4A331D86073A00B0C45C /* TableOfContents.swift */; }; 20 | 66F0320F1D8336C70094B9C9 /* RuntimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F0320E1D8336C70094B9C9 /* RuntimeTests.swift */; }; 21 | 66FD38AA1DE9077D00D68325 /* PlanTokenizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FD38A81DE9074F00D68325 /* PlanTokenizerTests.swift */; }; 22 | C710F276904FDDD62C08D114 /* Pods_Catalog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A3C8AF0F5A920387CA568 /* Pods_Catalog.framework */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 66251F161D360C65006EBC89 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 66876C531D25ED610054CED4 /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 66876C5A1D25ED610054CED4; 31 | remoteInfo = Catalog; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 386A3C8AF0F5A920387CA568 /* Pods_Catalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Catalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 461501D72C29FCDB7E2578A1 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 5156D58B56524CD92B7066B1 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 39 | 5558CC01A3BE72C10A6F983C /* Pods-Catalog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Catalog.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-Catalog/Pods-Catalog.debug.xcconfig"; sourceTree = ""; }; 40 | 661369701D5BE7CB00F6830F /* CompositionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CompositionTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 41 | 661CD8081DE4E97200841D14 /* NamedPlanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NamedPlanTests.swift; sourceTree = ""; }; 42 | 6624C6421D466D7C00DF3108 /* ContinuousPerformingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinuousPerformingTests.swift; sourceTree = ""; }; 43 | 66251F111D360C65006EBC89 /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 66876C5B1D25ED610054CED4 /* Catalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Catalog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 66876C5F1D25ED610054CED4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 46 | 66876C611D25ED610054CED4 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 47 | 66876C621D25ED610054CED4 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 48 | 66876C6A1D25ED610054CED4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 66876C6D1D25ED610054CED4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 66876C6F1D25ED610054CED4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 669256F11DD6134C00F659E6 /* TimelineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTests.swift; sourceTree = ""; }; 52 | 669F4A331D86073A00B0C45C /* TableOfContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableOfContents.swift; path = ../../TableOfContents.swift; sourceTree = ""; }; 53 | 66F0320E1D8336C70094B9C9 /* RuntimeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeTests.swift; sourceTree = ""; }; 54 | 66FD38A81DE9074F00D68325 /* PlanTokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlanTokenizerTests.swift; sourceTree = ""; }; 55 | B3FE7343838D7E9758F374DA /* Pods-Catalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Catalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-Catalog/Pods-Catalog.release.xcconfig"; sourceTree = ""; }; 56 | D53F8AA7C25DE6E5BF9FCE99 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 66251F0E1D360C65006EBC89 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | 12D431C606F2CA5213DFE9E2 /* Pods_UnitTests.framework in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 66876C581D25ED610054CED4 /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | C710F276904FDDD62C08D114 /* Pods_Catalog.framework in Frameworks */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | 66251F121D360C65006EBC89 /* tests */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 66251F1B1D360C87006EBC89 /* unit */, 83 | ); 84 | name = tests; 85 | path = ../../../tests; 86 | sourceTree = ""; 87 | }; 88 | 66251F1B1D360C87006EBC89 /* unit */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 66F0320E1D8336C70094B9C9 /* RuntimeTests.swift */, 92 | 661CD8081DE4E97200841D14 /* NamedPlanTests.swift */, 93 | 669256F11DD6134C00F659E6 /* TimelineTests.swift */, 94 | 661369701D5BE7CB00F6830F /* CompositionTests.swift */, 95 | 6624C6421D466D7C00DF3108 /* ContinuousPerformingTests.swift */, 96 | 66FD38A81DE9074F00D68325 /* PlanTokenizerTests.swift */, 97 | ); 98 | path = unit; 99 | sourceTree = ""; 100 | }; 101 | 66876C521D25ED610054CED4 = { 102 | isa = PBXGroup; 103 | children = ( 104 | 669F4A331D86073A00B0C45C /* TableOfContents.swift */, 105 | 66876C5D1D25ED610054CED4 /* app */, 106 | 66251F121D360C65006EBC89 /* tests */, 107 | 66876C5C1D25ED610054CED4 /* Products */, 108 | 6C7FF3FBAF18AED62BAC97E7 /* Pods */, 109 | B72F52CE7F045171734C3946 /* Frameworks */, 110 | ); 111 | sourceTree = ""; 112 | }; 113 | 66876C5C1D25ED610054CED4 /* Products */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 66876C5B1D25ED610054CED4 /* Catalog.app */, 117 | 66251F111D360C65006EBC89 /* UnitTests.xctest */, 118 | ); 119 | name = Products; 120 | sourceTree = ""; 121 | }; 122 | 66876C5D1D25ED610054CED4 /* app */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 66876C611D25ED610054CED4 /* AppDelegate.h */, 126 | 66876C621D25ED610054CED4 /* AppDelegate.m */, 127 | 66876C6A1D25ED610054CED4 /* Assets.xcassets */, 128 | 66876C6C1D25ED610054CED4 /* LaunchScreen.storyboard */, 129 | 66876C6F1D25ED610054CED4 /* Info.plist */, 130 | 66876C5E1D25ED610054CED4 /* Supporting Files */, 131 | ); 132 | name = app; 133 | path = src; 134 | sourceTree = ""; 135 | }; 136 | 66876C5E1D25ED610054CED4 /* Supporting Files */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 66876C5F1D25ED610054CED4 /* main.m */, 140 | ); 141 | name = "Supporting Files"; 142 | sourceTree = ""; 143 | }; 144 | 6C7FF3FBAF18AED62BAC97E7 /* Pods */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 5558CC01A3BE72C10A6F983C /* Pods-Catalog.debug.xcconfig */, 148 | B3FE7343838D7E9758F374DA /* Pods-Catalog.release.xcconfig */, 149 | 5156D58B56524CD92B7066B1 /* Pods-UnitTests.debug.xcconfig */, 150 | D53F8AA7C25DE6E5BF9FCE99 /* Pods-UnitTests.release.xcconfig */, 151 | ); 152 | name = Pods; 153 | sourceTree = ""; 154 | }; 155 | B72F52CE7F045171734C3946 /* Frameworks */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 386A3C8AF0F5A920387CA568 /* Pods_Catalog.framework */, 159 | 461501D72C29FCDB7E2578A1 /* Pods_UnitTests.framework */, 160 | ); 161 | name = Frameworks; 162 | sourceTree = ""; 163 | }; 164 | /* End PBXGroup section */ 165 | 166 | /* Begin PBXNativeTarget section */ 167 | 66251F101D360C65006EBC89 /* UnitTests */ = { 168 | isa = PBXNativeTarget; 169 | buildConfigurationList = 66251F1A1D360C65006EBC89 /* Build configuration list for PBXNativeTarget "UnitTests" */; 170 | buildPhases = ( 171 | 1B261BCF04C64618840CFCE4 /* [CP] Check Pods Manifest.lock */, 172 | 66251F0D1D360C65006EBC89 /* Sources */, 173 | 66251F0E1D360C65006EBC89 /* Frameworks */, 174 | 66251F0F1D360C65006EBC89 /* Resources */, 175 | 0643F0100C9AD302892B5E0C /* [CP] Embed Pods Frameworks */, 176 | E55B0F3BFFB0C8FABAE508F8 /* [CP] Copy Pods Resources */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 66251F171D360C65006EBC89 /* PBXTargetDependency */, 182 | ); 183 | name = UnitTests; 184 | productName = UnitTests; 185 | productReference = 66251F111D360C65006EBC89 /* UnitTests.xctest */; 186 | productType = "com.apple.product-type.bundle.unit-test"; 187 | }; 188 | 66876C5A1D25ED610054CED4 /* Catalog */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 66876C721D25ED610054CED4 /* Build configuration list for PBXNativeTarget "Catalog" */; 191 | buildPhases = ( 192 | 8A92F4D4135631D09243A581 /* [CP] Check Pods Manifest.lock */, 193 | 66876C571D25ED610054CED4 /* Sources */, 194 | 66876C581D25ED610054CED4 /* Frameworks */, 195 | 66876C591D25ED610054CED4 /* Resources */, 196 | 0EB3429332078EAF02EB0F71 /* [CP] Embed Pods Frameworks */, 197 | 8BDB827EE0BEE9E10A5BF76F /* [CP] Copy Pods Resources */, 198 | ); 199 | buildRules = ( 200 | ); 201 | dependencies = ( 202 | ); 203 | name = Catalog; 204 | productName = Catalog; 205 | productReference = 66876C5B1D25ED610054CED4 /* Catalog.app */; 206 | productType = "com.apple.product-type.application"; 207 | }; 208 | /* End PBXNativeTarget section */ 209 | 210 | /* Begin PBXProject section */ 211 | 66876C531D25ED610054CED4 /* Project object */ = { 212 | isa = PBXProject; 213 | attributes = { 214 | LastSwiftUpdateCheck = 0730; 215 | LastUpgradeCheck = 0800; 216 | ORGANIZATIONNAME = Google; 217 | TargetAttributes = { 218 | 66251F101D360C65006EBC89 = { 219 | CreatedOnToolsVersion = 7.3.1; 220 | DevelopmentTeam = ZGWD8QJUPK; 221 | LastSwiftMigration = 0800; 222 | }; 223 | 66876C5A1D25ED610054CED4 = { 224 | CreatedOnToolsVersion = 7.3.1; 225 | DevelopmentTeam = ZGWD8QJUPK; 226 | LastSwiftMigration = 0800; 227 | }; 228 | }; 229 | }; 230 | buildConfigurationList = 66876C561D25ED610054CED4 /* Build configuration list for PBXProject "Catalog" */; 231 | compatibilityVersion = "Xcode 3.2"; 232 | developmentRegion = English; 233 | hasScannedForEncodings = 0; 234 | knownRegions = ( 235 | en, 236 | Base, 237 | ); 238 | mainGroup = 66876C521D25ED610054CED4; 239 | productRefGroup = 66876C5C1D25ED610054CED4 /* Products */; 240 | projectDirPath = ""; 241 | projectRoot = ""; 242 | targets = ( 243 | 66876C5A1D25ED610054CED4 /* Catalog */, 244 | 66251F101D360C65006EBC89 /* UnitTests */, 245 | ); 246 | }; 247 | /* End PBXProject section */ 248 | 249 | /* Begin PBXResourcesBuildPhase section */ 250 | 66251F0F1D360C65006EBC89 /* Resources */ = { 251 | isa = PBXResourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | 66876C591D25ED610054CED4 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | 66876C6E1D25ED610054CED4 /* LaunchScreen.storyboard in Resources */, 262 | 66876C6B1D25ED610054CED4 /* Assets.xcassets in Resources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | /* End PBXResourcesBuildPhase section */ 267 | 268 | /* Begin PBXShellScriptBuildPhase section */ 269 | 0643F0100C9AD302892B5E0C /* [CP] Embed Pods Frameworks */ = { 270 | isa = PBXShellScriptBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | ); 274 | inputPaths = ( 275 | ); 276 | name = "[CP] Embed Pods Frameworks"; 277 | outputPaths = ( 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | shellPath = /bin/sh; 281 | shellScript = "\"${SRCROOT}/../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests-frameworks.sh\"\n"; 282 | showEnvVarsInLog = 0; 283 | }; 284 | 0EB3429332078EAF02EB0F71 /* [CP] Embed Pods Frameworks */ = { 285 | isa = PBXShellScriptBuildPhase; 286 | buildActionMask = 2147483647; 287 | files = ( 288 | ); 289 | inputPaths = ( 290 | ); 291 | name = "[CP] Embed Pods Frameworks"; 292 | outputPaths = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | shellPath = /bin/sh; 296 | shellScript = "\"${SRCROOT}/../../../Pods/Target Support Files/Pods-Catalog/Pods-Catalog-frameworks.sh\"\n"; 297 | showEnvVarsInLog = 0; 298 | }; 299 | 1B261BCF04C64618840CFCE4 /* [CP] Check Pods Manifest.lock */ = { 300 | isa = PBXShellScriptBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | ); 304 | inputPaths = ( 305 | ); 306 | name = "[CP] Check Pods Manifest.lock"; 307 | outputPaths = ( 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | shellPath = /bin/sh; 311 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 312 | showEnvVarsInLog = 0; 313 | }; 314 | 8A92F4D4135631D09243A581 /* [CP] Check Pods Manifest.lock */ = { 315 | isa = PBXShellScriptBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | ); 319 | inputPaths = ( 320 | ); 321 | name = "[CP] Check Pods Manifest.lock"; 322 | outputPaths = ( 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | shellPath = /bin/sh; 326 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 327 | showEnvVarsInLog = 0; 328 | }; 329 | 8BDB827EE0BEE9E10A5BF76F /* [CP] Copy Pods Resources */ = { 330 | isa = PBXShellScriptBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | ); 334 | inputPaths = ( 335 | ); 336 | name = "[CP] Copy Pods Resources"; 337 | outputPaths = ( 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | shellPath = /bin/sh; 341 | shellScript = "\"${SRCROOT}/../../../Pods/Target Support Files/Pods-Catalog/Pods-Catalog-resources.sh\"\n"; 342 | showEnvVarsInLog = 0; 343 | }; 344 | E55B0F3BFFB0C8FABAE508F8 /* [CP] Copy Pods Resources */ = { 345 | isa = PBXShellScriptBuildPhase; 346 | buildActionMask = 2147483647; 347 | files = ( 348 | ); 349 | inputPaths = ( 350 | ); 351 | name = "[CP] Copy Pods Resources"; 352 | outputPaths = ( 353 | ); 354 | runOnlyForDeploymentPostprocessing = 0; 355 | shellPath = /bin/sh; 356 | shellScript = "\"${SRCROOT}/../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests-resources.sh\"\n"; 357 | showEnvVarsInLog = 0; 358 | }; 359 | /* End PBXShellScriptBuildPhase section */ 360 | 361 | /* Begin PBXSourcesBuildPhase section */ 362 | 66251F0D1D360C65006EBC89 /* Sources */ = { 363 | isa = PBXSourcesBuildPhase; 364 | buildActionMask = 2147483647; 365 | files = ( 366 | 669256F21DD6134C00F659E6 /* TimelineTests.swift in Sources */, 367 | 66F0320F1D8336C70094B9C9 /* RuntimeTests.swift in Sources */, 368 | 661369711D5BE7CB00F6830F /* CompositionTests.swift in Sources */, 369 | 6624C6431D466D7C00DF3108 /* ContinuousPerformingTests.swift in Sources */, 370 | 66FD38AA1DE9077D00D68325 /* PlanTokenizerTests.swift in Sources */, 371 | 661CD8091DE4E97200841D14 /* NamedPlanTests.swift in Sources */, 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | 66876C571D25ED610054CED4 /* Sources */ = { 376 | isa = PBXSourcesBuildPhase; 377 | buildActionMask = 2147483647; 378 | files = ( 379 | 66876C631D25ED610054CED4 /* AppDelegate.m in Sources */, 380 | 66876C601D25ED610054CED4 /* main.m in Sources */, 381 | 669F4A341D86073A00B0C45C /* TableOfContents.swift in Sources */, 382 | ); 383 | runOnlyForDeploymentPostprocessing = 0; 384 | }; 385 | /* End PBXSourcesBuildPhase section */ 386 | 387 | /* Begin PBXTargetDependency section */ 388 | 66251F171D360C65006EBC89 /* PBXTargetDependency */ = { 389 | isa = PBXTargetDependency; 390 | target = 66876C5A1D25ED610054CED4 /* Catalog */; 391 | targetProxy = 66251F161D360C65006EBC89 /* PBXContainerItemProxy */; 392 | }; 393 | /* End PBXTargetDependency section */ 394 | 395 | /* Begin PBXVariantGroup section */ 396 | 66876C6C1D25ED610054CED4 /* LaunchScreen.storyboard */ = { 397 | isa = PBXVariantGroup; 398 | children = ( 399 | 66876C6D1D25ED610054CED4 /* Base */, 400 | ); 401 | name = LaunchScreen.storyboard; 402 | sourceTree = ""; 403 | }; 404 | /* End PBXVariantGroup section */ 405 | 406 | /* Begin XCBuildConfiguration section */ 407 | 66251F181D360C65006EBC89 /* Debug */ = { 408 | isa = XCBuildConfiguration; 409 | baseConfigurationReference = 5156D58B56524CD92B7066B1 /* Pods-UnitTests.debug.xcconfig */; 410 | buildSettings = { 411 | CLANG_ENABLE_MODULES = YES; 412 | DEVELOPMENT_TEAM = ZGWD8QJUPK; 413 | INFOPLIST_FILE = UnitTests/Info.plist; 414 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 415 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 416 | PRODUCT_BUNDLE_IDENTIFIER = com.google.UnitTests; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 419 | SWIFT_VERSION = 3.0; 420 | }; 421 | name = Debug; 422 | }; 423 | 66251F191D360C65006EBC89 /* Release */ = { 424 | isa = XCBuildConfiguration; 425 | baseConfigurationReference = D53F8AA7C25DE6E5BF9FCE99 /* Pods-UnitTests.release.xcconfig */; 426 | buildSettings = { 427 | CLANG_ENABLE_MODULES = YES; 428 | DEVELOPMENT_TEAM = ZGWD8QJUPK; 429 | INFOPLIST_FILE = UnitTests/Info.plist; 430 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 431 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 432 | PRODUCT_BUNDLE_IDENTIFIER = com.google.UnitTests; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SWIFT_VERSION = 3.0; 435 | }; 436 | name = Release; 437 | }; 438 | 66876C701D25ED610054CED4 /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_SEARCH_USER_PATHS = NO; 442 | CLANG_ANALYZER_NONNULL = YES; 443 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 444 | CLANG_CXX_LIBRARY = "libc++"; 445 | CLANG_ENABLE_MODULES = YES; 446 | CLANG_ENABLE_OBJC_ARC = YES; 447 | CLANG_WARN_BOOL_CONVERSION = YES; 448 | CLANG_WARN_CONSTANT_CONVERSION = YES; 449 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 450 | CLANG_WARN_EMPTY_BODY = YES; 451 | CLANG_WARN_ENUM_CONVERSION = YES; 452 | CLANG_WARN_INFINITE_RECURSION = YES; 453 | CLANG_WARN_INT_CONVERSION = YES; 454 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 455 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 456 | CLANG_WARN_UNREACHABLE_CODE = YES; 457 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 458 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 459 | COPY_PHASE_STRIP = NO; 460 | DEBUG_INFORMATION_FORMAT = dwarf; 461 | ENABLE_STRICT_OBJC_MSGSEND = YES; 462 | ENABLE_TESTABILITY = YES; 463 | GCC_C_LANGUAGE_STANDARD = gnu99; 464 | GCC_DYNAMIC_NO_PIC = NO; 465 | GCC_NO_COMMON_BLOCKS = YES; 466 | GCC_OPTIMIZATION_LEVEL = 0; 467 | GCC_PREPROCESSOR_DEFINITIONS = ( 468 | "DEBUG=1", 469 | "$(inherited)", 470 | ); 471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 473 | GCC_WARN_UNDECLARED_SELECTOR = YES; 474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 475 | GCC_WARN_UNUSED_FUNCTION = YES; 476 | GCC_WARN_UNUSED_VARIABLE = YES; 477 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 478 | MTL_ENABLE_DEBUG_INFO = YES; 479 | ONLY_ACTIVE_ARCH = YES; 480 | SDKROOT = iphoneos; 481 | TARGETED_DEVICE_FAMILY = "1,2"; 482 | }; 483 | name = Debug; 484 | }; 485 | 66876C711D25ED610054CED4 /* Release */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | ALWAYS_SEARCH_USER_PATHS = NO; 489 | CLANG_ANALYZER_NONNULL = YES; 490 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 491 | CLANG_CXX_LIBRARY = "libc++"; 492 | CLANG_ENABLE_MODULES = YES; 493 | CLANG_ENABLE_OBJC_ARC = YES; 494 | CLANG_WARN_BOOL_CONVERSION = YES; 495 | CLANG_WARN_CONSTANT_CONVERSION = YES; 496 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 497 | CLANG_WARN_EMPTY_BODY = YES; 498 | CLANG_WARN_ENUM_CONVERSION = YES; 499 | CLANG_WARN_INFINITE_RECURSION = YES; 500 | CLANG_WARN_INT_CONVERSION = YES; 501 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 502 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 503 | CLANG_WARN_UNREACHABLE_CODE = YES; 504 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 505 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 506 | COPY_PHASE_STRIP = NO; 507 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 508 | ENABLE_NS_ASSERTIONS = NO; 509 | ENABLE_STRICT_OBJC_MSGSEND = YES; 510 | GCC_C_LANGUAGE_STANDARD = gnu99; 511 | GCC_NO_COMMON_BLOCKS = YES; 512 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 513 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 514 | GCC_WARN_UNDECLARED_SELECTOR = YES; 515 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 516 | GCC_WARN_UNUSED_FUNCTION = YES; 517 | GCC_WARN_UNUSED_VARIABLE = YES; 518 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 519 | MTL_ENABLE_DEBUG_INFO = NO; 520 | SDKROOT = iphoneos; 521 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 522 | TARGETED_DEVICE_FAMILY = "1,2"; 523 | VALIDATE_PRODUCT = YES; 524 | }; 525 | name = Release; 526 | }; 527 | 66876C731D25ED610054CED4 /* Debug */ = { 528 | isa = XCBuildConfiguration; 529 | baseConfigurationReference = 5558CC01A3BE72C10A6F983C /* Pods-Catalog.debug.xcconfig */; 530 | buildSettings = { 531 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 532 | CLANG_ENABLE_MODULES = YES; 533 | DEVELOPMENT_TEAM = ZGWD8QJUPK; 534 | INFOPLIST_FILE = src/Info.plist; 535 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 536 | PRODUCT_BUNDLE_IDENTIFIER = com.google.Catalog; 537 | PRODUCT_NAME = Catalog; 538 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 539 | SWIFT_VERSION = 3.0; 540 | }; 541 | name = Debug; 542 | }; 543 | 66876C741D25ED610054CED4 /* Release */ = { 544 | isa = XCBuildConfiguration; 545 | baseConfigurationReference = B3FE7343838D7E9758F374DA /* Pods-Catalog.release.xcconfig */; 546 | buildSettings = { 547 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 548 | CLANG_ENABLE_MODULES = YES; 549 | DEVELOPMENT_TEAM = ZGWD8QJUPK; 550 | INFOPLIST_FILE = src/Info.plist; 551 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 552 | PRODUCT_BUNDLE_IDENTIFIER = com.google.Catalog; 553 | PRODUCT_NAME = Catalog; 554 | SWIFT_VERSION = 3.0; 555 | }; 556 | name = Release; 557 | }; 558 | /* End XCBuildConfiguration section */ 559 | 560 | /* Begin XCConfigurationList section */ 561 | 66251F1A1D360C65006EBC89 /* Build configuration list for PBXNativeTarget "UnitTests" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 66251F181D360C65006EBC89 /* Debug */, 565 | 66251F191D360C65006EBC89 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | 66876C561D25ED610054CED4 /* Build configuration list for PBXProject "Catalog" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | 66876C701D25ED610054CED4 /* Debug */, 574 | 66876C711D25ED610054CED4 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | 66876C721D25ED610054CED4 /* Build configuration list for PBXNativeTarget "Catalog" */ = { 580 | isa = XCConfigurationList; 581 | buildConfigurations = ( 582 | 66876C731D25ED610054CED4 /* Debug */, 583 | 66876C741D25ED610054CED4 /* Release */, 584 | ); 585 | defaultConfigurationIsVisible = 0; 586 | defaultConfigurationName = Release; 587 | }; 588 | /* End XCConfigurationList section */ 589 | }; 590 | rootObject = 66876C531D25ED610054CED4 /* Project object */; 591 | } 592 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog.xcodeproj/xcshareddata/xcschemes/Catalog.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/apps/Catalog/Catalog.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 | 49 | 50 | 52 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/apps/Catalog/UnitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/apps/Catalog/src/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @interface AppDelegate : UIResponder 20 | @end 21 | -------------------------------------------------------------------------------- /examples/apps/Catalog/src/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "AppDelegate.h" 18 | 19 | @import CatalogByConvention; 20 | 21 | @interface AppDelegate () 22 | @end 23 | 24 | @implementation AppDelegate 25 | 26 | @synthesize window; 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | CBCNodeListViewController *rootViewController = [[CBCNodeListViewController alloc] initWithNode:CBCCreateNavigationTree()]; 31 | rootViewController.title = @"Material Motion Runtime Catalog"; 32 | self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /examples/apps/Catalog/src/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /examples/apps/Catalog/src/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 | -------------------------------------------------------------------------------- /examples/apps/Catalog/src/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 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 | -------------------------------------------------------------------------------- /examples/apps/Catalog/src/main.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | #import "AppDelegate.h" 19 | 20 | int main(int argc, char* argv[]) { 21 | @autoreleasepool { 22 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/MDMConsoleLoggingTracer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | #import "MDMTracing.h" 20 | 21 | /** 22 | An instance of a logging tracer added to a MotionRuntime instance will output all Tracing 23 | invocation names and parameters to the console. 24 | */ 25 | NS_SWIFT_NAME(ConsoleLoggingTracer) 26 | @interface MDMConsoleLoggingTracer : NSObject 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /src/MDMConsoleLoggingTracer.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMConsoleLoggingTracer.h" 18 | 19 | #import 20 | 21 | static NSString *debugDescriptionOfPlanProperties(NSObject *plan) { 22 | NSMutableArray *propertyDescriptions = [NSMutableArray array]; 23 | unsigned int numberOfProperties = 0; 24 | objc_property_t *properties = class_copyPropertyList([plan class], &numberOfProperties); 25 | for (unsigned int ix = 0; ix < numberOfProperties; ix++) { 26 | objc_property_t property = properties[ix]; 27 | const char *propName = property_getName(property); 28 | if (propName) { 29 | NSString *propertyName = [NSString stringWithCString:propName 30 | encoding:[NSString defaultCStringEncoding]]; 31 | const char *attributes = property_getAttributes(property); 32 | NSString *propertyType = [NSString stringWithCString:attributes 33 | encoding:[NSString defaultCStringEncoding]]; 34 | NSArray *propertyTypeComponents = [propertyType componentsSeparatedByString:@"\""]; 35 | if ([propertyTypeComponents count] > 1) { 36 | propertyType = propertyTypeComponents[1]; 37 | } else { 38 | propertyType = @"@"; 39 | } 40 | 41 | [propertyDescriptions addObject:[NSString stringWithFormat: 42 | @" %@: %@ = %@", 43 | propertyName, 44 | propertyType, 45 | [plan valueForKey:propertyName]]]; 46 | } 47 | } 48 | free(properties); 49 | 50 | return [propertyDescriptions componentsJoinedByString:@"\n"]; 51 | } 52 | 53 | @implementation MDMConsoleLoggingTracer 54 | 55 | - (void)didAddPlan:(NSObject *)plan to:(id)target { 56 | NSLog(@"didAddPlan to target: %@\nPlan: %@\n%@\n\n", 57 | target, 58 | NSStringFromClass([plan class]), 59 | debugDescriptionOfPlanProperties(plan)); 60 | } 61 | 62 | - (void)didAddPlan:(id)plan named:(NSString *)name to:(id)target { 63 | NSLog(@"didAddPlan named %@ to target: %@\nPlan: %@\n%@\n\n", 64 | name, 65 | target, 66 | NSStringFromClass([plan class]), 67 | debugDescriptionOfPlanProperties(plan)); 68 | } 69 | 70 | - (void)didRemovePlanNamed:(NSString *)name from:(id)target { 71 | NSLog(@"didRemovePlan named %@ from target: %@\n\n", name, target); 72 | } 73 | 74 | - (void)didCreatePerformer:(NSObject *)performer for:(id)target { 75 | NSLog(@"didCreatePerformer: %@ for: %@\n\n", NSStringFromClass([performer class]), target); 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /src/MDMMotionRuntime.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @protocol MDMMotionRuntimeDelegate; 20 | @protocol MDMPlan; 21 | @protocol MDMNamedPlan; 22 | @protocol MDMTracing; 23 | 24 | /** 25 | An instance of MDMMotionRuntime acts as the mediating agent between plans and performers. 26 | 27 | Plans are objects that conform to the MDMPlan protocol. 28 | Performers are objects that conform to the MDMPerforming protocol. 29 | 30 | ## Usage 31 | 32 | Many runtime instances may be instantiated throughout the lifetime of an app. Generally-speaking, 33 | one runtime is created per interaction. An interaction might be a transition, a one-off animation, 34 | or a complex multi-state interaction. 35 | 36 | Plans can be associated with targets by using addPlan:to:. 37 | 38 | The runtime creates performer instances when plans are added. Performers are expected to fulfill 39 | the provided plans. 40 | 41 | ## Lifecycle 42 | 43 | When an instance of a runtime is deallocated its performers will also be deallocated. 44 | */ 45 | NS_SWIFT_NAME(MotionRuntime) 46 | @interface MDMMotionRuntime : NSObject 47 | 48 | #pragma mark Adding plans 49 | 50 | /** Associate a plan with a given target. */ 51 | - (void)addPlan:(nonnull NSObject *)plan to:(nonnull id)target 52 | NS_SWIFT_NAME(addPlan(_:to:)); 53 | 54 | /** Associate plans with a given target. */ 55 | - (void)addPlans:(nonnull NSArray *> *)plans to:(nonnull id)target 56 | NS_SWIFT_NAME(addPlans(_:to:)); 57 | 58 | /** 59 | Associates a named plan with a given target. 60 | 61 | @param plan The plan to add. 62 | @param name String identifier for the plan. 63 | @param target The target on which the plan can operate. 64 | */ 65 | - (void)addPlan:(nonnull id)plan 66 | named:(nonnull NSString *)name 67 | to:(nonnull id)target 68 | NS_SWIFT_NAME(addPlan(_:named:to:)); 69 | 70 | /** 71 | Removes any plan associated with the given name on the given target. 72 | 73 | @param name String identifier for the plan. 74 | @param target The target on which the plan can operate. 75 | */ 76 | - (void)removePlanNamed:(nonnull NSString *)name 77 | from:(nonnull id)target 78 | NS_SWIFT_NAME(removePlan(named:from:)); 79 | 80 | #pragma mark Tracing 81 | 82 | /** 83 | Registers a tracer with the runtime. 84 | 85 | The tracer will be strongly held by the runtime. 86 | */ 87 | - (void)addTracer:(nonnull id)tracer 88 | NS_SWIFT_NAME(addTracer(_:)); 89 | 90 | /** 91 | Removes a tracer from the runtime. 92 | 93 | Does nothing if the tracer is not currently associated with the runtime. 94 | */ 95 | - (void)removeTracer:(nonnull id)tracer 96 | NS_SWIFT_NAME(removeTracer(_:)); 97 | 98 | /** Returns the list of registered tracers. */ 99 | - (nonnull NSArray> *)tracers; 100 | 101 | #pragma mark State 102 | 103 | /** 104 | Whether or not this runtime is active. 105 | 106 | A runtime is active only if at least performer currently owns a non-terminated token. 107 | */ 108 | @property(nonatomic, assign, readonly, getter=isActive) BOOL active; 109 | 110 | #pragma mark Delegated events 111 | 112 | /** A runtime delegate can listen to specific state change events. */ 113 | @property(nonatomic, weak, nullable) id delegate; 114 | 115 | @end 116 | 117 | /** 118 | The MDMMotionRuntimeDelegate protocol defines state change events that may be sent from an instance of 119 | MDMMotionRuntime. 120 | */ 121 | NS_SWIFT_NAME(MotionRuntimeDelegate) 122 | @protocol MDMMotionRuntimeDelegate 123 | 124 | /** Informs the receiver that the runtime's current activity state has changed. */ 125 | - (void)motionRuntimeActivityStateDidChange:(nonnull MDMMotionRuntime *)runtime; 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /src/MDMMotionRuntime.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMMotionRuntime.h" 18 | 19 | #import "MDMTracing.h" 20 | #import "private/MDMTargetRegistry.h" 21 | #import "private/MDMTargetScope.h" 22 | #import "private/MDMToken.h" 23 | #import "private/MDMTokenPool.h" 24 | 25 | @interface MDMMotionRuntime () 26 | 27 | @property(nonatomic, strong, readonly) NSMutableSet> *activeTokens; 28 | 29 | @end 30 | 31 | @implementation MDMMotionRuntime { 32 | MDMTargetRegistry *_targetRegistry; 33 | NSMutableOrderedSet> *_tracers; 34 | } 35 | 36 | - (instancetype)init { 37 | self = [super init]; 38 | if (self) { 39 | _tracers = [NSMutableOrderedSet orderedSet]; 40 | _targetRegistry = [[MDMTargetRegistry alloc] initWithRuntime:self tracers:_tracers]; 41 | _activeTokens = [NSMutableSet set]; 42 | } 43 | return self; 44 | } 45 | 46 | #pragma mark - Private 47 | 48 | - (void)willAddPlan:(NSObject *)plan { 49 | MDMToken *token = (MDMToken *)[_targetRegistry.tokenPool tokenForPlan:plan]; 50 | [token addActivityObserver:self]; 51 | } 52 | 53 | - (void)stateDidChange { 54 | if ([self.delegate respondsToSelector:@selector(motionRuntimeActivityStateDidChange:)]) { 55 | [self.delegate motionRuntimeActivityStateDidChange:self]; 56 | } 57 | } 58 | 59 | #pragma mark - MDMTokenActivityObserving 60 | 61 | - (void)tokenDidActivate:(MDMToken *)token { 62 | BOOL wasInactive = _activeTokens.count == 0; 63 | 64 | [_activeTokens addObject:token]; 65 | 66 | if (wasInactive) { 67 | [self stateDidChange]; 68 | } 69 | } 70 | 71 | - (void)tokenDidDeactivate:(MDMToken *)token { 72 | NSAssert([_activeTokens containsObject:token], 73 | @"Token is not active. May have already been terminated by a previous invocation."); 74 | 75 | [_activeTokens removeObject:token]; 76 | 77 | if (_activeTokens.count == 0) { 78 | [self stateDidChange]; 79 | } 80 | } 81 | 82 | #pragma mark - Public 83 | 84 | - (BOOL)isActive { 85 | return self.activeTokens.count > 0; 86 | } 87 | 88 | - (void)addPlan:(NSObject *)plan to:(id)target { 89 | NSObject *copiedPlan = [plan copy]; 90 | [self willAddPlan:copiedPlan]; 91 | [_targetRegistry addPlan:copiedPlan to:target]; 92 | } 93 | 94 | - (void)addPlans:(nonnull NSArray *> *)plans to:(nonnull id)target { 95 | for (NSObject *plan in plans) { 96 | [self addPlan:plan to:target]; 97 | } 98 | } 99 | 100 | - (void)addPlan:(NSObject *)plan named:(NSString *)name to:(id)target { 101 | NSParameterAssert(name.length > 0); 102 | NSObject *copiedPlan = [plan copy]; 103 | [self willAddPlan:(NSObject *)copiedPlan]; 104 | [_targetRegistry addPlan:copiedPlan named:name to:target]; 105 | } 106 | 107 | - (void)removePlanNamed:(NSString *)name from:(id)target { 108 | NSParameterAssert(name.length > 0); 109 | [_targetRegistry removePlanNamed:name from:target]; 110 | } 111 | 112 | - (void)addTracer:(nonnull id)tracer { 113 | [_tracers addObject:tracer]; 114 | } 115 | 116 | - (void)removeTracer:(nonnull id)tracer { 117 | [_tracers removeObject:tracer]; 118 | } 119 | 120 | - (nonnull NSArray> *)tracers { 121 | return _tracers.array; 122 | } 123 | @end 124 | -------------------------------------------------------------------------------- /src/MDMPerforming.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @protocol MDMPlan; 20 | @protocol MDMNamedPlan; 21 | 22 | /** 23 | A class conforming to MDMPerforming is expected to implement the plan of motion described by objects 24 | that conform to MDMPlan. 25 | */ 26 | NS_SWIFT_NAME(Performing) 27 | @protocol MDMPerforming 28 | 29 | #pragma mark Designated initializer 30 | 31 | /** The receiver is expected to execute its plan to the provided target. */ 32 | - (nonnull instancetype)initWithTarget:(nonnull id)target; 33 | 34 | #pragma mark Adding plans to a performer 35 | 36 | /** 37 | Provides the performer with a plan. 38 | 39 | The performer may choose to store this plan or to simply extract necessary information and cache 40 | it separately. 41 | 42 | @param plan The plan that required this type of performer. 43 | */ 44 | - (void)addPlan:(nonnull id)plan 45 | NS_SWIFT_NAME(addPlan(_:)); 46 | 47 | @end 48 | 49 | /** Specifics for a named plan performer to allow named plans to be added and removed. */ 50 | NS_SWIFT_NAME(NamedPlanPerforming) 51 | @protocol MDMNamedPlanPerforming 52 | 53 | /** 54 | Provides the performer with a plan and an associated name. 55 | 56 | @param plan The plan that required this type of performer. 57 | @param name The name by which the plan can be identified. 58 | */ 59 | - (void)addPlan:(nonnull id)plan 60 | named:(nonnull NSString *)name 61 | NS_SWIFT_NAME(addPlan(_:named:)); 62 | 63 | /** 64 | Removes a named plan from a performer. 65 | 66 | @param name The name by which the plan can be identified. 67 | */ 68 | - (void)removePlanNamed:(nonnull NSString *)name 69 | NS_SWIFT_NAME(removePlan(named:)); 70 | 71 | @end 72 | 73 | #pragma mark - Continuous performing 74 | 75 | @protocol MDMPlanTokenizing; 76 | 77 | /** 78 | A performer that conforms to MDMContinuousPerforming is able to fetch tokens for plans. 79 | 80 | The runtime uses these tokens to inform its active state. If any token is active then the runtime 81 | is active. Otherwise, the runtime is idle. 82 | 83 | The performer should store a strong reference to the token generator. Fetch a token when a plan is 84 | added. When some continuous work is about to begin, such as adding an animation or starting a 85 | gesture recognizer, active the token. Deactivate the token when the continuous work completes, such 86 | as when an animation reaches its resting state or when a gesture recognizer is ended or canceled. 87 | */ 88 | NS_SWIFT_NAME(ContinuousPerforming) 89 | @protocol MDMContinuousPerforming 90 | 91 | #pragma mark Continuous performing 92 | 93 | /** 94 | Provides the performer with a plan tokenizer instance. 95 | 96 | Invoked before any add(plan:) invocations occur. 97 | */ 98 | - (void)givePlanTokenizer:(nonnull id)planTokenizer 99 | NS_SWIFT_NAME(givePlanTokenizer(_:)); 100 | 101 | @end 102 | 103 | /** 104 | A token is a representation of potential activity for a specific plan. 105 | 106 | When activity for a plan begins, the token should be activated. When the activity ends, the token 107 | should be deactivated. Tokens can be reactivated. 108 | 109 | Tokens will deactivate themselves on dealloc. 110 | */ 111 | NS_SWIFT_NAME(Tokened) 112 | @protocol MDMTokened 113 | 114 | #pragma mark Modifying token active state 115 | 116 | /** An active token will be added to the runtime's pool of active tokens. */ 117 | @property(nonatomic, assign, getter=isActive) BOOL active; 118 | 119 | @end 120 | 121 | /** A tokenizer turns plans into motion tokens. */ 122 | NS_SWIFT_NAME(PlanTokenizing) 123 | @protocol MDMPlanTokenizing 124 | 125 | /** 126 | Returns a token for a given plan. 127 | 128 | The receiver of this token is expected to activate the token when work associated with the plan 129 | has started. Similarly, the token should be deactivated when the work completes. 130 | 131 | May fail to generate a token if the performer's runtime has been deallocated. 132 | 133 | Will always return the same token instance for a given plan instance. 134 | */ 135 | - (nullable id)tokenForPlan:(nonnull id)plan; 136 | 137 | @end 138 | 139 | #pragma mark - Composition 140 | 141 | /** 142 | A plan emitter allows an object to emit new plans to a backing runtime for the target to which the 143 | performer is associated. 144 | */ 145 | NS_SWIFT_NAME(PlanEmitting) 146 | @protocol MDMPlanEmitting 147 | 148 | /** Emit a new plan. The plan will immediately be added to the backing runtime. */ 149 | - (void)emitPlan:(nonnull NSObject *)plan 150 | NS_SWIFT_NAME(emitPlan(_:)); 151 | 152 | @end 153 | 154 | /** A class conforming to MDMComposablePerforming is able to commit new plans. */ 155 | NS_SWIFT_NAME(ComposablePerforming) 156 | @protocol MDMComposablePerforming 157 | 158 | #pragma mark Composable performing 159 | 160 | /** The performer is provided a plan emitter shortly after initialization. */ 161 | - (void)setPlanEmitter:(nonnull id)planEmitter 162 | NS_SWIFT_NAME(setPlanEmitter(_:)); 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /src/MDMPlan.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | /** 20 | A class conforming to MDMPlan is expected to describe a plan of motion for a target. 21 | 22 | Plans are translated into performers by an instance of MDMMotionRuntime. 23 | */ 24 | NS_SWIFT_NAME(Plan) 25 | @protocol MDMPlan 26 | 27 | #pragma mark Defining the performer class 28 | 29 | /** 30 | Asks the receiver to return a class conforming to MDMPerformer. 31 | 32 | The returned class will be instantiated by an instance of MDMMotionRuntime. The instantiated performer is 33 | expected to execute the plan. 34 | */ 35 | - (nonnull Class)performerClass; 36 | 37 | @end 38 | 39 | /** 40 | Instances of `MDMNamedPlan` can be added or removed as named plans to or from targets. 41 | */ 42 | NS_SWIFT_NAME(NamedPlan) 43 | @protocol MDMNamedPlan 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /src/MDMTimeline.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @protocol MDMTimelineObserving; 20 | 21 | /** A scrubber can be attached to a timeline in order to control the timeline's timeOffset. */ 22 | NS_SWIFT_NAME(TimelineScrubber) 23 | @interface MDMTimelineScrubber : NSObject 24 | 25 | /** Unavailable. Use timeline.attachScrubberWithTimeOffset instead. */ 26 | - (nonnull instancetype)init NS_UNAVAILABLE; 27 | 28 | /** Unavailable. Use timeline.attachScrubberWithTimeOffset instead. */ 29 | + (nonnull instancetype) new NS_UNAVAILABLE; 30 | 31 | /** The desired time offset. */ 32 | @property(nonatomic, assign) NSTimeInterval timeOffset; 33 | 34 | @end 35 | 36 | /** A timeline provides an API for scrubbing time. */ 37 | NS_SWIFT_NAME(Timeline) 38 | @interface MDMTimeline : NSObject 39 | 40 | /** Populates beginTime with the current absolute time. Can only be invoked once. */ 41 | - (void)begin; 42 | 43 | /** The time at which the timeline began, if it has. */ 44 | @property(nonatomic, strong, nullable, readonly) NSNumber *beginTime; 45 | 46 | /** 47 | Attaches a scrubber to the timeline if one wasn't already attached. 48 | 49 | Sends a didAttachScrubber event to all observers if a scrubber was attached. 50 | 51 | If a scrubber was already attached then the existing scrubber's timeOffset will be modified 52 | instead. 53 | */ 54 | - (void)attachScrubberWithTimeOffset:(NSTimeInterval)timeOffset; 55 | 56 | /** 57 | Detaches a scrubber from the timeline if one was already attached. 58 | 59 | Sends a didDetachScrubber event to all observers. 60 | */ 61 | - (void)detachScrubber; 62 | 63 | /** 64 | Returns the scrubber if one is attached. 65 | 66 | Modifications to the scrubber's timeOffset will generate scrubberDidScrub events on all observers. 67 | */ 68 | @property(nonatomic, strong, nullable, readonly) MDMTimelineScrubber *scrubber; 69 | 70 | /** Add a timeline observer to the timeline. */ 71 | - (void)addTimelineObserver:(nonnull id)observer; 72 | 73 | /** Remove a timeline observer from the timeline. */ 74 | - (void)removeTimelineObserver:(nonnull id)observer; 75 | 76 | @end 77 | 78 | /** A timeline observer may receive events in relation to state changes from a Timeline instance. */ 79 | NS_SWIFT_NAME(TimelineObserving) 80 | @protocol MDMTimelineObserving 81 | 82 | /** Informs the receiver that a scrubber has been attached to a timeline. */ 83 | - (void)timeline:(nonnull MDMTimeline *)timeline didAttachScrubber:(nonnull MDMTimelineScrubber *)scrubber; 84 | 85 | /** Informs the receiver that a scrubber has been detached from a timeline. */ 86 | - (void)timeline:(nonnull MDMTimeline *)timeline didDetachScrubber:(nonnull MDMTimelineScrubber *)scrubber; 87 | 88 | /** Informs the receiver that a timeline scrubber's timeOffset has changed. */ 89 | - (void)timeline:(nonnull MDMTimeline *)timeline scrubberDidScrub:(NSTimeInterval)timeOffset; 90 | 91 | @end 92 | -------------------------------------------------------------------------------- /src/MDMTimeline.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTimeline.h" 18 | 19 | #import 20 | 21 | @interface MDMTimeline () 22 | - (void)scrubberDidScrub:(NSTimeInterval)timeOffset; 23 | @end 24 | 25 | @interface MDMTimelineScrubber () 26 | @property(nonatomic, weak) MDMTimeline *timeline; 27 | @end 28 | 29 | @implementation MDMTimelineScrubber 30 | 31 | - (instancetype)initWithTimeline:(MDMTimeline *)timeline { 32 | self = [super init]; 33 | if (self) { 34 | _timeline = timeline; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)setTimeOffset:(NSTimeInterval)timeOffset { 40 | if (_timeOffset == timeOffset) { 41 | return; 42 | } 43 | 44 | _timeOffset = timeOffset; 45 | 46 | [self.timeline scrubberDidScrub:_timeOffset]; 47 | } 48 | 49 | @end 50 | 51 | @implementation MDMTimeline { 52 | MDMTimelineScrubber *_scrubber; 53 | NSHashTable *_observers; 54 | BOOL _isScrubberAttached; 55 | } 56 | 57 | - (instancetype)init { 58 | self = [super init]; 59 | if (self) { 60 | _scrubber = [[MDMTimelineScrubber alloc] initWithTimeline:self]; 61 | 62 | _observers = [NSHashTable weakObjectsHashTable]; 63 | } 64 | return self; 65 | } 66 | 67 | #pragma mark - Public 68 | 69 | - (void)begin { 70 | NSAssert(_beginTime == nil, @"Begin was already invoked on this timeline."); 71 | _beginTime = @(CACurrentMediaTime()); 72 | } 73 | 74 | - (void)attachScrubberWithTimeOffset:(NSTimeInterval)timeOffset { 75 | if (!_isScrubberAttached) { 76 | for (id observer in _observers) { 77 | [observer timeline:self didAttachScrubber:_scrubber]; 78 | } 79 | _isScrubberAttached = true; 80 | } 81 | 82 | _scrubber.timeOffset = timeOffset; 83 | } 84 | 85 | - (void)detachScrubber { 86 | if (!_isScrubberAttached) { 87 | return; 88 | } 89 | _isScrubberAttached = false; 90 | 91 | for (id observer in _observers) { 92 | [observer timeline:self didDetachScrubber:_scrubber]; 93 | } 94 | } 95 | 96 | - (MDMTimelineScrubber *)scrubber { 97 | return _isScrubberAttached ? _scrubber : nil; 98 | } 99 | 100 | - (void)addTimelineObserver:(id)observer { 101 | [_observers addObject:observer]; 102 | } 103 | 104 | - (void)removeTimelineObserver:(nonnull id)observer { 105 | [_observers removeObject:observer]; 106 | } 107 | 108 | - (void)scrubberDidScrub:(NSTimeInterval)timeOffset { 109 | if (!_isScrubberAttached) { 110 | return; 111 | } 112 | for (id observer in _observers) { 113 | [observer timeline:self scrubberDidScrub:timeOffset]; 114 | } 115 | } 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /src/MDMTracing.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @protocol MDMPerforming; 20 | @protocol MDMPlan; 21 | @protocol MDMNamedPlan; 22 | 23 | /** 24 | A tracer object may implement a variety of hooks for the purposes of observing changes to the 25 | internal workings of a runtime. 26 | */ 27 | NS_SWIFT_NAME(Tracing) 28 | @protocol MDMTracing 29 | @optional 30 | 31 | /** Invoked after a plan has been added to the runtime. */ 32 | - (void)didAddPlan:(nonnull id)plan to:(nonnull id)target 33 | NS_SWIFT_NAME(didAddPlan(_:to:)); 34 | 35 | /** Invoked after a named plan has been added to the runtime. */ 36 | - (void)didAddPlan:(nonnull id)plan named:(nonnull NSString *)name to:(nonnull id)target 37 | NS_SWIFT_NAME(didAddPlan(_:named:to:)); 38 | 39 | /** Invoked when a named plan is removed from the runtime. */ 40 | - (void)didRemovePlanNamed:(nonnull NSString *)name from:(nonnull id)target 41 | NS_SWIFT_NAME(didRemovePlanNamed(_:from:)); 42 | 43 | /** Invoked after a performer has been created by the runtime. */ 44 | - (void)didCreatePerformer:(nonnull id)performer for:(nonnull id)target 45 | NS_SWIFT_NAME(didCreatePerformer(_:for:)); 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /src/MaterialMotionRuntime.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMConsoleLoggingTracer.h" 18 | #import "MDMMotionRuntime.h" 19 | #import "MDMPerforming.h" 20 | #import "MDMPlan.h" 21 | #import "MDMTimeline.h" 22 | #import "MDMTracing.h" 23 | -------------------------------------------------------------------------------- /src/private/MDMPlanEmitter.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMPerforming.h" 18 | 19 | @class MDMTargetRegistry; 20 | 21 | @interface MDMPlanEmitter : NSObject 22 | 23 | /** Initialize a newly allocated emitter with the provided runtime and target. */ 24 | - (nonnull instancetype)initWithTargetRegistry:(nonnull MDMTargetRegistry *)targetRegistry 25 | target:(nonnull id)target; 26 | 27 | - (nonnull instancetype)init NS_UNAVAILABLE; 28 | + (nonnull instancetype) new NS_UNAVAILABLE; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /src/private/MDMPlanEmitter.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMPlanEmitter.h" 18 | 19 | #import "MDMMotionRuntime.h" 20 | #import "MDMTargetRegistry.h" 21 | 22 | @interface MDMPlanEmitter () 23 | 24 | @property(nonatomic, weak) MDMTargetRegistry *targetRegistry; 25 | @property(nonatomic, weak) id target; 26 | 27 | @end 28 | 29 | @implementation MDMPlanEmitter 30 | 31 | - (instancetype)initWithTargetRegistry:(MDMTargetRegistry *)targetRegistry target:(id)target { 32 | self = [super init]; 33 | if (self) { 34 | self.targetRegistry = targetRegistry; 35 | self.target = target; 36 | } 37 | return self; 38 | } 39 | 40 | #pragma mark - MDMPlanEmitting 41 | 42 | - (void)emitPlan:(NSObject *)plan { 43 | MDMTargetRegistry *registry = self.targetRegistry; 44 | id target = self.target; 45 | if (!registry || !target) { 46 | return; 47 | } 48 | [registry.runtime addPlan:plan to:target]; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /src/private/MDMTargetRegistry.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @class MDMTokenPool; 20 | @class MDMMotionRuntime; 21 | @protocol MDMPlan; 22 | @protocol MDMNamedPlan; 23 | @protocol MDMTracing; 24 | 25 | @interface MDMTargetRegistry : NSObject 26 | 27 | - (nonnull instancetype)initWithRuntime:(nonnull MDMMotionRuntime *)runtime 28 | tracers:(nonnull NSOrderedSet> *)tracers 29 | NS_DESIGNATED_INITIALIZER; 30 | 31 | - (nonnull instancetype)init NS_UNAVAILABLE; 32 | + (nonnull instancetype) new NS_UNAVAILABLE; 33 | 34 | @property(nonatomic, weak, nullable, readonly) MDMMotionRuntime *runtime; 35 | 36 | @property(nonatomic, strong, nonnull, readonly) MDMTokenPool *tokenPool; 37 | 38 | - (void)addPlan:(nonnull NSObject *)plan to:(nonnull id)target; 39 | 40 | - (void)addPlan:(nonnull id)plan 41 | named:(nonnull NSString *)name 42 | to:(nonnull id)target; 43 | - (void)removePlanNamed:(nonnull NSString *)name 44 | from:(nonnull id)target; 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /src/private/MDMTargetRegistry.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTargetRegistry.h" 18 | 19 | #import "MDMPlanEmitter.h" 20 | #import "MDMTargetScope.h" 21 | #import "MDMTokenPool.h" 22 | 23 | @implementation MDMTargetRegistry { 24 | NSMapTable *_targetToScope; 25 | NSOrderedSet> *_tracers; 26 | } 27 | 28 | - (instancetype)initWithRuntime:(nonnull MDMMotionRuntime *)runtime 29 | tracers:(nonnull NSOrderedSet> *)tracers { 30 | self = [super init]; 31 | if (self) { 32 | _runtime = runtime; 33 | _tracers = tracers; 34 | 35 | _targetToScope = [NSMapTable weakToStrongObjectsMapTable]; 36 | _tokenPool = [MDMTokenPool new]; 37 | } 38 | return self; 39 | } 40 | 41 | #pragma mark - Private 42 | 43 | - (MDMTargetScope *)scopeForTarget:(id)target { 44 | MDMTargetScope *scope = [_targetToScope objectForKey:target]; 45 | if (!scope) { 46 | MDMPlanEmitter *emitter = [[MDMPlanEmitter alloc] initWithTargetRegistry:self target:target]; 47 | scope = [[MDMTargetScope alloc] initWithTarget:target 48 | tracers:_tracers 49 | planEmitter:emitter 50 | tokenPool:_tokenPool]; 51 | [_targetToScope setObject:scope forKey:target]; 52 | } 53 | 54 | return scope; 55 | } 56 | 57 | #pragma mark - Public 58 | 59 | - (void)addPlan:(NSObject *)plan to:(id)target { 60 | [[self scopeForTarget:target] addPlan:plan to:target]; 61 | } 62 | 63 | - (void)addPlan:(NSObject *)plan named:(NSString *)name to:(id)target { 64 | NSParameterAssert(name.length > 0); 65 | [[self scopeForTarget:target] addPlan:plan named:name to:target]; 66 | } 67 | 68 | - (void)removePlanNamed:(NSString *)name from:(id)target { 69 | NSParameterAssert(name.length > 0); 70 | [[self scopeForTarget:target] removePlanNamed:name from:target]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /src/private/MDMTargetScope.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @class MDMTokenPool; 20 | @class MDMPlanEmitter; 21 | @protocol MDMPlan; 22 | @protocol MDMNamedPlan; 23 | @protocol MDMTracing; 24 | 25 | /** An entity responsible for managing the performers associated with a given target. */ 26 | @interface MDMTargetScope : NSObject 27 | 28 | - (nonnull instancetype)initWithTarget:(nonnull id)target 29 | tracers:(nonnull NSOrderedSet> *)tracers 30 | planEmitter:(nonnull MDMPlanEmitter *)planEmitter 31 | tokenPool:(nonnull MDMTokenPool *)tokenPool 32 | NS_DESIGNATED_INITIALIZER; 33 | 34 | - (nonnull instancetype)init NS_UNAVAILABLE; 35 | + (nonnull instancetype) new NS_UNAVAILABLE; 36 | 37 | - (void)addPlan:(nonnull id)plan to:(nonnull id)target; 38 | 39 | - (void)addPlan:(nonnull id)plan named:(nonnull NSString *)name to:(nonnull id)target; 40 | - (void)removePlanNamed:(nonnull NSString *)name from:(nonnull id)target; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /src/private/MDMTargetScope.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTargetScope.h" 18 | 19 | #import "MDMPlan.h" 20 | #import "MDMPlanEmitter.h" 21 | #import "MDMTokenPool.h" 22 | #import "MDMTracing.h" 23 | 24 | @implementation MDMTargetScope { 25 | id _target; 26 | NSMutableDictionary> *_performerClassNameToPerformer; 27 | NSMutableDictionary> *_performerPlanNameToPerformer; 28 | MDMTokenPool *_tokenPool; 29 | NSOrderedSet> *_tracers; 30 | MDMPlanEmitter *_planEmitter; 31 | } 32 | 33 | - (instancetype)initWithTarget:(id)target 34 | tracers:(NSOrderedSet> *)tracers 35 | planEmitter:(MDMPlanEmitter *)planEmitter 36 | tokenPool:(MDMTokenPool *)tokenPool { 37 | self = [super init]; 38 | if (self) { 39 | _target = target; 40 | _tracers = tracers; 41 | _planEmitter = planEmitter; 42 | _tokenPool = tokenPool; 43 | _performerClassNameToPerformer = [NSMutableDictionary dictionary]; 44 | _performerPlanNameToPerformer = [NSMutableDictionary dictionary]; 45 | } 46 | return self; 47 | } 48 | 49 | - (void)addPlan:(NSObject *)plan to:(id)target { 50 | BOOL isNew = NO; 51 | id performer = [self findOrCreatePerformerForPlan:plan isNew:&isNew]; 52 | 53 | if (isNew) { 54 | [self notifyPerformerCreation:performer target:target]; 55 | } 56 | 57 | [performer addPlan:plan]; 58 | 59 | for (id tracer in _tracers) { 60 | if ([tracer respondsToSelector:@selector(didAddPlan:to:)]) { 61 | [tracer didAddPlan:plan to:target]; 62 | } 63 | } 64 | } 65 | 66 | - (void)addPlan:(NSObject *)plan named:(NSString *)name to:(id)target { 67 | id performer = (id)_performerPlanNameToPerformer[name]; 68 | [self removePlanNamed:name from:target withPerformer:performer]; 69 | 70 | BOOL isNew = NO; 71 | performer = [self findOrCreatePerformerForNamedPlan:plan named:name isNew:&isNew]; 72 | _performerPlanNameToPerformer[name] = performer; 73 | if (isNew) { 74 | [self notifyPerformerCreation:performer target:target]; 75 | } 76 | 77 | if ([performer respondsToSelector:@selector(addPlan:named:)]) { 78 | [performer addPlan:plan named:name]; 79 | } 80 | for (id tracer in _tracers) { 81 | if ([tracer respondsToSelector:@selector(didAddPlan:named:to:)]) { 82 | [tracer didAddPlan:plan named:name to:target]; 83 | } 84 | } 85 | } 86 | 87 | - (void)removePlanNamed:(NSString *)name from:(id)target { 88 | id performer = (id)_performerPlanNameToPerformer[name]; 89 | [self removePlanNamed:name from:target withPerformer:performer]; 90 | } 91 | 92 | #pragma mark - Private 93 | 94 | - (void)removePlanNamed:(NSString *)name from:(id)target withPerformer:(id)performer { 95 | if (performer != nil) { 96 | if ([performer respondsToSelector:@selector(removePlanNamed:)]) { 97 | [(id)performer removePlanNamed:name]; 98 | } 99 | [_performerPlanNameToPerformer removeObjectForKey:name]; 100 | for (id tracer in _tracers) { 101 | if ([tracer respondsToSelector:@selector(didRemovePlanNamed:from:)]) { 102 | [tracer didRemovePlanNamed:name from:target]; 103 | } 104 | } 105 | } 106 | } 107 | 108 | - (id)findOrCreatePerformerForNamedPlan:(id)plan 109 | named:(NSString *)name 110 | isNew:(BOOL *)isNew { 111 | id performer = (id)_performerPlanNameToPerformer[name]; 112 | if (performer) { 113 | *isNew = NO; 114 | return performer; 115 | } 116 | performer = (id)[self findOrCreatePerformerForPlan:plan isNew:isNew]; 117 | _performerPlanNameToPerformer[name] = performer; 118 | return performer; 119 | } 120 | 121 | - (id)findOrCreatePerformerForPlan:(id)plan isNew:(BOOL *)isNew { 122 | Class performerClass = [plan performerClass]; 123 | id performerClassName = NSStringFromClass(performerClass); 124 | id performer = _performerClassNameToPerformer[performerClassName]; 125 | if (performer) { 126 | *isNew = NO; 127 | return performer; 128 | } 129 | performer = [[performerClass alloc] initWithTarget:_target]; 130 | _performerClassNameToPerformer[performerClassName] = performer; 131 | [self setUpFeaturesForPerformer:performer]; 132 | *isNew = YES; 133 | return performer; 134 | } 135 | 136 | - (void)setUpFeaturesForPerformer:(id)performer { 137 | // Composable performance 138 | if ([performer respondsToSelector:@selector(setPlanEmitter:)]) { 139 | id composablePerformer = (id)performer; 140 | [composablePerformer setPlanEmitter:_planEmitter]; 141 | } 142 | 143 | // Continuous performance 144 | if ([performer respondsToSelector:@selector(givePlanTokenizer:)]) { 145 | id continuousPerformer = (id)performer; 146 | [continuousPerformer givePlanTokenizer:_tokenPool]; 147 | } 148 | } 149 | 150 | - (void)notifyPerformerCreation:(id)performer target:(id)target { 151 | for (id tracer in _tracers) { 152 | if ([tracer respondsToSelector:@selector(didCreatePerformer:for:)]) { 153 | [tracer didCreatePerformer:performer for:_target]; 154 | } 155 | } 156 | } 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /src/private/MDMToken+Private.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMToken.h" 18 | 19 | @interface MDMToken () 20 | 21 | - (instancetype)initInternal; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /src/private/MDMToken.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMPerforming.h" 18 | 19 | @protocol MDMTokenActivityObserving; 20 | 21 | @interface MDMToken : NSObject 22 | 23 | - (nonnull instancetype)init NS_UNAVAILABLE; 24 | + (nonnull instancetype) new NS_UNAVAILABLE; 25 | 26 | - (void)addActivityObserver:(nonnull id)observer; 27 | 28 | @end 29 | 30 | @protocol MDMTokenActivityObserving 31 | 32 | - (void)tokenDidActivate:(nonnull MDMToken *)token; 33 | - (void)tokenDidDeactivate:(nonnull MDMToken *)token; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /src/private/MDMToken.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMToken.h" 18 | #import "MDMToken+Private.h" 19 | 20 | #import "MDMPlan.h" 21 | 22 | @implementation MDMToken { 23 | NSHashTable> *_observers; 24 | } 25 | 26 | @synthesize active = _active; 27 | 28 | - (void)dealloc { 29 | self.active = false; 30 | } 31 | 32 | - (instancetype)initInternal { 33 | self = [super init]; 34 | if (self) { 35 | _observers = [NSHashTable weakObjectsHashTable]; 36 | } 37 | return self; 38 | } 39 | 40 | - (void)setActive:(BOOL)active { 41 | if (_active == active) { 42 | return; 43 | } 44 | 45 | _active = active; 46 | 47 | if (_active) { 48 | for (id observer in _observers) { 49 | [observer tokenDidActivate:self]; 50 | } 51 | } else { 52 | for (id observer in _observers) { 53 | [observer tokenDidDeactivate:self]; 54 | } 55 | } 56 | } 57 | 58 | - (void)addActivityObserver:(nonnull id)observer { 59 | [_observers addObject:observer]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /src/private/MDMTokenPool.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMPerforming.h" 18 | 19 | @interface MDMTokenPool : NSObject 20 | @end 21 | -------------------------------------------------------------------------------- /src/private/MDMTokenPool.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #import "MDMTokenPool.h" 18 | 19 | #import "MDMPlan.h" 20 | #import "MDMToken+Private.h" 21 | 22 | @implementation MDMTokenPool { 23 | NSMapTable, id> *_planToToken; 24 | } 25 | 26 | - (instancetype)init { 27 | self = [super init]; 28 | if (self) { 29 | _planToToken = [NSMapTable weakToStrongObjectsMapTable]; 30 | } 31 | return self; 32 | } 33 | 34 | - (id)tokenForPlan:(id)plan { 35 | // Performers that can't be continuous can never generate tokens. 36 | if (![[plan performerClass] instancesRespondToSelector:@selector(givePlanTokenizer:)]) { 37 | return nil; 38 | } 39 | id token = [_planToToken objectForKey:plan]; 40 | if (!token) { 41 | token = [[MDMToken alloc] initInternal]; 42 | [_planToToken setObject:token forKey:plan]; 43 | } 44 | return token; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /tests/src/Emit.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import MaterialMotionRuntime 19 | 20 | /** A plan that emits an arbitrary plan. */ 21 | public class Emit: NSObject, Plan { 22 | public var plan: Plan 23 | public init(plan: Plan) { 24 | self.plan = plan 25 | } 26 | 27 | public func performerClass() -> AnyClass { 28 | return Performer.self 29 | } 30 | 31 | public func copy(with zone: NSZone? = nil) -> Any { 32 | return Emit(plan: plan) 33 | } 34 | 35 | private class Performer: NSObject, ComposablePerforming { 36 | let target: Any 37 | required init(target: Any) { 38 | self.target = target 39 | } 40 | 41 | func addPlan(_ plan: Plan) { 42 | let emit = plan as! Emit 43 | emitter.emitPlan(emit.plan) 44 | } 45 | 46 | var emitter: PlanEmitting! 47 | func setPlanEmitter(_ planEmitter: PlanEmitting) { 48 | emitter = planEmitter 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/src/ExpectableRuntimeDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | public class ExpectableRuntimeDelegate: NSObject, MotionRuntimeDelegate { 22 | public var activityStateDidChange = false 23 | public var didIdleExpectation: XCTestExpectation? 24 | 25 | public func motionRuntimeActivityStateDidChange(_ runtime: MotionRuntime) { 26 | activityStateDidChange = true 27 | 28 | if !runtime.isActive { 29 | didIdleExpectation?.fulfill() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/src/ForeverActive.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import MaterialMotionRuntime 19 | 20 | /** A plan that will cause a runtime to be active forever. */ 21 | public class ForeverActive: NSObject, Plan { 22 | public func performerClass() -> AnyClass { 23 | return Performer.self 24 | } 25 | 26 | public func copy(with zone: NSZone? = nil) -> Any { 27 | return ForeverActive() 28 | } 29 | 30 | private class Performer: NSObject, ContinuousPerforming { 31 | let target: Any 32 | required init(target: Any) { 33 | self.target = target 34 | } 35 | 36 | func addPlan(_ plan: Plan) { 37 | tokenizer.token(for: plan)?.isActive = true 38 | } 39 | 40 | var tokenizer: PlanTokenizing! 41 | func givePlanTokenizer(_ tokenizer: PlanTokenizing) { 42 | self.tokenizer = tokenizer 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/src/InstantlyInactive.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import MaterialMotionRuntime 19 | 20 | /** A plan that causes an is-active token to be immediately generated and terminated. */ 21 | public class InstantlyInactive: NSObject, Plan { 22 | public func performerClass() -> AnyClass { 23 | return Performer.self 24 | } 25 | 26 | public func copy(with zone: NSZone? = nil) -> Any { 27 | return InstantlyInactive() 28 | } 29 | 30 | private class Performer: NSObject, ContinuousPerforming { 31 | let target: Any 32 | required init(target: Any) { 33 | self.target = target 34 | } 35 | 36 | public func addPlan(_ plan: Plan) { 37 | let token = tokenizer.token(for: plan)! 38 | token.isActive = true 39 | token.isActive = false 40 | } 41 | 42 | var tokenizer: PlanTokenizing! 43 | func givePlanTokenizer(_ tokenizer: PlanTokenizing) { 44 | self.tokenizer = tokenizer 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/src/RuntimeSpy.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import MaterialMotionRuntime 19 | 20 | /** 21 | A fuzzy representation of a RuntimeTracing event. 22 | 23 | Plans are copied and performers are implementation details, so this event enum allows you to 24 | describe runtime tracing events in a fuzzy manner. 25 | */ 26 | public enum RuntimeSpyTestEvent { 27 | /** Equates to any didAddPlan event with a matching plan type and target. */ 28 | case didAddPlan(plan: AnyClass, target: AnyObject) 29 | 30 | /** Equates to any didAddPlan:named event with a matching plan type, name, and target. */ 31 | case didAddPlanNamed(plan: AnyClass, name: String, target: AnyObject) 32 | 33 | /** Equates to any didRemovePlanedNamed event with a matching name and target. */ 34 | case didRemovePlanNamed(name: String, target: AnyObject) 35 | 36 | /** Equates to any didCreatePerformer event with a matching target. */ 37 | case didCreatePerformer(target: AnyObject) 38 | } 39 | 40 | /** 41 | A runtime spy logs the events received by a runtime instance and exposes methods for checking 42 | the logged events in a fuzzy manner. 43 | */ 44 | public class RuntimeSpy: NSObject { 45 | var events: [RuntimeSpyEvent] = [] 46 | 47 | /** 48 | Checks for exact equality. 49 | 50 | Use sparingly because new events may start being logged in the future. 51 | */ 52 | public func isEqual(to: [RuntimeSpyTestEvent]) -> Bool { 53 | return events == to 54 | } 55 | 56 | /** 57 | Returns the number of event instances that match the provided event. 58 | 59 | This is the preferred mechanism a unit test can make use of to validate existance of events. 60 | */ 61 | public func countOf(_ event: RuntimeSpyTestEvent) -> Int { 62 | return events.filter { $0 == event }.count 63 | } 64 | } 65 | 66 | extension RuntimeSpy: Tracing { 67 | public func didAddPlan(_ plan: Plan, to target: Any) { 68 | events.append(.didAddPlan(plan: plan, target: target)) 69 | } 70 | 71 | public func didAddPlan(_ plan: NamedPlan, named name: String, to target: Any) { 72 | events.append(.didAddPlanNamed(plan: plan, name: name, target: target)) 73 | } 74 | 75 | public func didRemovePlanNamed(_ name: String, from target: Any) { 76 | events.append(.didRemovePlanNamed(name: name, target: target)) 77 | } 78 | 79 | public func didCreatePerformer(_ performer: Performing, for target: Any) { 80 | events.append(.didCreatePerformer(performer: performer, target: target)) 81 | } 82 | } 83 | 84 | enum RuntimeSpyEvent { 85 | case didAddPlan(plan: Plan, target: Any) 86 | case didAddPlanNamed(plan: NamedPlan, name: String, target: Any) 87 | case didRemovePlanNamed(name: String, target: Any) 88 | case didCreatePerformer(performer: Performing, target: Any) 89 | } 90 | 91 | // Fuzzy comparator of MotionRuntime Tracing events with their public equivalent. 92 | func ==(lhs: RuntimeSpyEvent, rhs: RuntimeSpyTestEvent) -> Bool { 93 | switch (lhs, rhs) { 94 | case (.didAddPlan(let plan1, let target1), 95 | .didAddPlan(let plan2, let target2)) 96 | where plan1.isMember(of: plan2) && (target1 as AnyObject) === target2: 97 | return true 98 | case (.didAddPlanNamed(let plan1, let name1, let target1), 99 | .didAddPlanNamed(let plan2, let name2, let target2)) 100 | where plan1.isMember(of: plan2) && name1 == name2 && (target1 as AnyObject) === target2: 101 | return true 102 | case (.didRemovePlanNamed(let name1, let target1), 103 | .didRemovePlanNamed(let name2, let target2)) 104 | where name1 == name2 && (target1 as AnyObject) === target2: 105 | return true 106 | case (.didCreatePerformer(_, let target1), 107 | .didCreatePerformer(let target2)) 108 | where (target1 as AnyObject) === target2: 109 | return true 110 | default: return false 111 | } 112 | } 113 | 114 | func ==(lhs: [RuntimeSpyEvent], rhs: [RuntimeSpyTestEvent]) -> Bool { 115 | if lhs.count != rhs.count { 116 | return false 117 | } 118 | for index in 0.. Bool { 48 | switch (lhs, rhs) { 49 | case (.didAttach, .didAttach): return true 50 | case (.didDetach, .didDetach): return true 51 | case (.didScrub(let timeOffset1), .didScrub(let timeOffset2)) where timeOffset1 == timeOffset2: 52 | return true 53 | default: return false 54 | } 55 | } 56 | 57 | public func ==(lhs: [TimelineSpyEvent], rhs: [TimelineSpyEvent]) -> Bool { 58 | if lhs.count != rhs.count { 59 | return false 60 | } 61 | for index in 0.. AnyClass { 24 | return Performer.self 25 | } 26 | 27 | public func copy(with zone: NSZone? = nil) -> Any { 28 | return ViewTargetAltering() 29 | } 30 | 31 | private class Performer: NSObject, NamedPlanPerforming { 32 | let target: Any 33 | required init(target: Any) { 34 | self.target = target 35 | } 36 | 37 | func addPlan(_ plan: Plan) { 38 | if let unwrappedTarget = self.target as? UITextView { 39 | unwrappedTarget.text = unwrappedTarget.text + "addInvoked" 40 | } 41 | } 42 | 43 | func addPlan(_ plan: NamedPlan, named name: String) { 44 | if let unwrappedTarget = self.target as? UITextView { 45 | unwrappedTarget.text = unwrappedTarget.text + "addPlanInvoked" 46 | } 47 | } 48 | 49 | func removePlan(named name: String) { 50 | if let unwrappedTarget = self.target as? UITextView { 51 | unwrappedTarget.text = unwrappedTarget.text + "removePlanInvoked" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/CompositionTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | // Tests related to the composition of plans. 22 | class CompositionTests: XCTestCase { 23 | 24 | func testComposedDelegationCausesActivityStateChange() { 25 | let runtime = MotionRuntime() 26 | 27 | let delegate = ExpectableRuntimeDelegate() 28 | runtime.delegate = delegate 29 | 30 | runtime.addPlan(Emit(plan: InstantlyInactive()), to: NSObject()) 31 | 32 | // The following steps are now expected to have occurred: 33 | // 34 | // 1. The Emit plan was committed to the runtime. 35 | // 2. The Emit plan's performer emitted the InstantlyInactive plan. 36 | // 3. The InstantlyInactive plan changed the runtime's activity state by immediately starting 37 | // and completing some delegated work. 38 | 39 | XCTAssertTrue(delegate.activityStateDidChange) 40 | XCTAssertFalse(runtime.isActive) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/ContinuousPerformingTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | // Tests related to continuous performers. 22 | class ContinuousPerformingTests: XCTestCase { 23 | 24 | func testContinuousPerformerCausesActivityStateChange() { 25 | let runtime = MotionRuntime() 26 | 27 | let delegate = ExpectableRuntimeDelegate() 28 | runtime.delegate = delegate 29 | 30 | runtime.addPlan(InstantlyInactive(), to: NSObject()) 31 | 32 | XCTAssertTrue(delegate.activityStateDidChange) 33 | XCTAssertFalse(runtime.isActive) 34 | } 35 | 36 | func testForeverActivePerformerCausesActivityStateChange() { 37 | let runtime = MotionRuntime() 38 | 39 | let delegate = ExpectableRuntimeDelegate() 40 | runtime.delegate = delegate 41 | 42 | runtime.addPlan(ForeverActive(), to: NSObject()) 43 | 44 | XCTAssertTrue(delegate.activityStateDidChange) 45 | XCTAssertTrue(runtime.isActive) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/NamedPlanTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | class NamedPlanTests: XCTestCase { 22 | 23 | var target:UITextView! 24 | private var incrementerTarget:IncrementerTarget! 25 | var firstViewTargetAlteringPlan:NamedPlan! 26 | 27 | override func tearDown() { 28 | target = nil 29 | incrementerTarget = nil 30 | firstViewTargetAlteringPlan = nil 31 | } 32 | 33 | override func setUp() { 34 | super.setUp() 35 | target = UITextView() 36 | incrementerTarget = IncrementerTarget() 37 | firstViewTargetAlteringPlan = ViewTargetAltering() 38 | target.text = "" 39 | } 40 | 41 | func testAddingNamedPlan() { 42 | let runtime = MotionRuntime() 43 | 44 | runtime.addPlan(firstViewTargetAlteringPlan, named: "common_name", to: target) 45 | 46 | XCTAssertTrue(target.text! == "addPlanInvoked") 47 | } 48 | 49 | func testAddAndRemoveTheSameNamedPlan() { 50 | let runtime = MotionRuntime() 51 | 52 | runtime.addPlan(firstViewTargetAlteringPlan, named: "name_one", to: target) 53 | runtime.removePlan(named: "name_one", from: target) 54 | 55 | XCTAssertTrue(target.text! == "addPlanInvokedremovePlanInvoked") 56 | } 57 | 58 | func testRemoveNamedPlanThatIsntThere() { 59 | let runtime = MotionRuntime() 60 | 61 | runtime.addPlan(firstViewTargetAlteringPlan, named: "common_name", to: target) 62 | runtime.removePlan(named: "was_never_added_plan", from: target) 63 | 64 | XCTAssertTrue(target.text! == "addPlanInvoked") 65 | } 66 | 67 | func testNamedPlansOverwiteOneAnother() { 68 | let runtime = MotionRuntime() 69 | let planA = IncrementerTargetPlan() 70 | let planB = IncrementerTargetPlan() 71 | runtime.addPlan(planA, named: "common_name", to: incrementerTarget) 72 | runtime.addPlan(planB, named: "common_name", to: incrementerTarget) 73 | 74 | XCTAssertTrue(incrementerTarget.addCounter == 2) 75 | XCTAssertTrue(incrementerTarget.removeCounter == 1) 76 | } 77 | 78 | func testNamedPlansMakeAddAndRemoveCallbacks() { 79 | let runtime = MotionRuntime() 80 | let plan = ViewTargetAltering() 81 | runtime.addPlan(plan, named: "one_name", to: target) 82 | runtime.removePlan(named: "one_name", from: target) 83 | runtime.addPlan(plan, named: "two_name", to: target) 84 | 85 | XCTAssertTrue(target.text! == "addPlanInvokedremovePlanInvokedaddPlanInvoked") 86 | } 87 | 88 | func testAddingTheSameNamedPlanTwiceToTheSameTarget() { 89 | let runtime = MotionRuntime() 90 | let plan = IncrementerTargetPlan() 91 | runtime.addPlan(plan, named: "one", to: incrementerTarget) 92 | runtime.addPlan(plan, named: "one", to: incrementerTarget) 93 | 94 | XCTAssertTrue(incrementerTarget.addCounter == 2) 95 | XCTAssertTrue(incrementerTarget.removeCounter == 1) 96 | } 97 | 98 | func testAddingTheSamePlanWithSimilarNamesToTheSameTarget() { 99 | let runtime = MotionRuntime() 100 | let firstPlan = IncrementerTargetPlan() 101 | runtime.addPlan(firstPlan, named: "one", to: incrementerTarget) 102 | runtime.addPlan(firstPlan, named: "One", to: incrementerTarget) 103 | runtime.addPlan(firstPlan, named: "1", to: incrementerTarget) 104 | runtime.addPlan(firstPlan, named: "ONE", to: incrementerTarget) 105 | 106 | XCTAssertTrue(incrementerTarget.addCounter == 4) 107 | XCTAssertTrue(incrementerTarget.removeCounter == 0) 108 | } 109 | 110 | func testAddingTheSameNamedPlanToDifferentTargets() { 111 | let runtime = MotionRuntime() 112 | let firstPlan = IncrementerTargetPlan() 113 | let secondIncrementerTarget = IncrementerTarget() 114 | runtime.addPlan(firstPlan, named: "one", to: incrementerTarget) 115 | runtime.addPlan(firstPlan, named: "one", to: secondIncrementerTarget) 116 | 117 | XCTAssertTrue(incrementerTarget.addCounter == 1) 118 | XCTAssertTrue(incrementerTarget.removeCounter == 0) 119 | XCTAssertTrue(secondIncrementerTarget.addCounter == 1) 120 | XCTAssertTrue(secondIncrementerTarget.removeCounter == 0) 121 | } 122 | 123 | func testNamedPlanOnlyInvokesNamedCallbacks() { 124 | let runtime = MotionRuntime() 125 | let plan = ViewTargetAltering() 126 | runtime.addPlan(plan, named: "one_name", to: target) 127 | 128 | XCTAssertTrue(target.text!.range(of: "addInvoked") == nil) 129 | } 130 | 131 | func testPlanOnlyInvokesPlanCallbacks() { 132 | let runtime = MotionRuntime() 133 | let plan = RegularPlanTargetAlteringPlan() 134 | runtime.addPlan(plan, to: target) 135 | 136 | XCTAssertTrue(target.text!.range(of: "addPlanInvoked") == nil) 137 | XCTAssertTrue(target.text!.range(of: "removePlanInvoked") == nil) 138 | } 139 | 140 | func testNamedPlansReusePerformers() { 141 | let runtime = MotionRuntime() 142 | let spy = RuntimeSpy() 143 | runtime.addTracer(spy) 144 | 145 | runtime.addPlan(firstViewTargetAlteringPlan, named: "name_one", to: target) 146 | runtime.removePlan(named: "name_one", from: target) 147 | 148 | XCTAssertEqual(spy.countOf(.didCreatePerformer(target: target)), 1) 149 | } 150 | 151 | func testNamedPlansAdditionsAreCommunicatedViaTracers() { 152 | let runtime = MotionRuntime() 153 | let spy = RuntimeSpy() 154 | runtime.addTracer(spy) 155 | 156 | runtime.addPlan(firstViewTargetAlteringPlan, named: "name_one", to: target) 157 | runtime.removePlan(named: "name_one", from: target) 158 | 159 | XCTAssertEqual(spy.countOf(.didAddPlanNamed(plan: ViewTargetAltering.self, 160 | name: "name_one", 161 | target: target)), 1) 162 | XCTAssertEqual(spy.countOf(.didRemovePlanNamed(name: "name_one", target: target)), 1) 163 | } 164 | 165 | func testNamedPlansRespectTracers() { 166 | let differentPlan = ChangeBooleanNamedPlan(desiredBoolean:true) 167 | let state = State() 168 | let runtime = MotionRuntime() 169 | let spy = RuntimeSpy() 170 | runtime.addTracer(spy) 171 | 172 | runtime.addPlan(firstViewTargetAlteringPlan, named: "name_one", to: target) 173 | runtime.addPlan(differentPlan, named: "name_two", to: state) 174 | 175 | XCTAssertEqual(spy.countOf(.didAddPlanNamed(plan: ViewTargetAltering.self, 176 | name: "name_one", 177 | target: target)), 1) 178 | XCTAssertEqual(spy.countOf(.didAddPlanNamed(plan: ChangeBooleanNamedPlan.self, 179 | name: "name_two", 180 | target: state)), 1) 181 | 182 | XCTAssert(target.text == "addPlanInvoked") 183 | XCTAssert(state.boolean) 184 | } 185 | 186 | func testPerformerCallbacksAreInvokedBeforeTracers() { 187 | let trackingPlan = RegularPlanTargetAlteringPlan() 188 | let state = State() 189 | let runtime = MotionRuntime() 190 | let spy = RuntimeSpy() 191 | runtime.addTracer(spy) 192 | 193 | runtime.addPlan(trackingPlan, named: "name_one", to: state) 194 | runtime.removePlan(named: "name_one", from: state) 195 | 196 | XCTAssert(spy.isEqual(to: [ 197 | .didCreatePerformer(target: state), 198 | .didAddPlanNamed(plan: RegularPlanTargetAlteringPlan.self, name: "name_one", target: state), 199 | .didRemovePlanNamed(name: "name_one", target: state), 200 | ])) 201 | } 202 | 203 | private class IncrementerTarget: NSObject { 204 | var addCounter = 0 205 | var removeCounter = 0 206 | } 207 | 208 | private class RegularPlanTargetAlteringPlan: NSObject, NamedPlan { 209 | 210 | func performerClass() -> AnyClass { 211 | return Performer.self 212 | } 213 | 214 | public func copy(with zone: NSZone? = nil) -> Any { 215 | return RegularPlanTargetAlteringPlan() 216 | } 217 | 218 | private class Performer: NSObject, NamedPlanPerforming { 219 | let target: Any 220 | required init(target: Any) { 221 | self.target = target 222 | } 223 | 224 | func addPlan(_ plan: Plan) { 225 | if let unwrappedTarget = self.target as? UITextView { 226 | unwrappedTarget.text = unwrappedTarget.text + "addInvoked" 227 | } 228 | } 229 | 230 | func addPlan(_ plan: NamedPlan, named name: String) { 231 | if let unwrappedTarget = self.target as? UITextView { 232 | unwrappedTarget.text = unwrappedTarget.text + "addPlanInvoked" 233 | } 234 | } 235 | 236 | func removePlan(named name: String) { 237 | if let unwrappedTarget = self.target as? UITextView { 238 | unwrappedTarget.text = unwrappedTarget.text + "removePlanInvoked" 239 | } 240 | } 241 | } 242 | } 243 | 244 | private class ChangeBooleanNamedPlan: NSObject, NamedPlan { 245 | var desiredBoolean: Bool 246 | 247 | init(desiredBoolean: Bool) { 248 | self.desiredBoolean = desiredBoolean 249 | } 250 | 251 | func performerClass() -> AnyClass { 252 | return Performer.self 253 | } 254 | 255 | public func copy(with zone: NSZone? = nil) -> Any { 256 | return ChangeBooleanNamedPlan(desiredBoolean: desiredBoolean) 257 | } 258 | 259 | private class Performer: NSObject, Performing, NamedPlanPerforming { 260 | let target: State 261 | required init(target: Any) { 262 | self.target = target as! State 263 | } 264 | 265 | public func addPlan(_ plan: Plan) { 266 | // No-op 267 | } 268 | 269 | func addPlan(_ plan: NamedPlan, named name: String) { 270 | let testPlan = plan as! ChangeBooleanNamedPlan 271 | target.boolean = testPlan.desiredBoolean 272 | } 273 | 274 | func removePlan(named name: String) { 275 | 276 | } 277 | } 278 | } 279 | 280 | private class IncrementerTargetPlan: NSObject, NamedPlan { 281 | 282 | func performerClass() -> AnyClass { 283 | return Performer.self 284 | } 285 | 286 | public func copy(with zone: NSZone? = nil) -> Any { 287 | return IncrementerTargetPlan() 288 | } 289 | 290 | private class Performer: NSObject, NamedPlanPerforming { 291 | let target: Any 292 | required init(target: Any) { 293 | self.target = target 294 | } 295 | 296 | public func addPlan(_ plan: Plan) { 297 | // No-op 298 | } 299 | 300 | func addPlan(_ plan: NamedPlan, named name: String) { 301 | if let unwrappedTarget = self.target as? IncrementerTarget { 302 | unwrappedTarget.addCounter = unwrappedTarget.addCounter + 1 303 | } 304 | } 305 | 306 | func removePlan(named name: String) { 307 | if let unwrappedTarget = self.target as? IncrementerTarget { 308 | unwrappedTarget.removeCounter = unwrappedTarget.removeCounter + 1 309 | } 310 | } 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /tests/unit/PlanTokenizerTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | class TokenPoolTests: XCTestCase { 22 | 23 | func testAlwaysSameTokenForSamePlan() { 24 | let runtime = MotionRuntime() 25 | runtime.addPlan(TokenFetching(), to: NSObject()) 26 | } 27 | 28 | private class TokenFetching: NSObject, Plan { 29 | func performerClass() -> AnyClass { 30 | return Performer.self 31 | } 32 | 33 | public func copy(with zone: NSZone? = nil) -> Any { 34 | return TokenFetching() 35 | } 36 | 37 | private class Performer: NSObject, ContinuousPerforming { 38 | let target: Any 39 | required init(target: Any) { 40 | self.target = target 41 | } 42 | 43 | func addPlan(_ plan: Plan) { 44 | let token1 = planTokenizer.token(for: plan)! 45 | let token2 = planTokenizer.token(for: plan)! 46 | XCTAssertTrue(token1 === token2) 47 | } 48 | 49 | var planTokenizer: PlanTokenizing! 50 | func givePlanTokenizer(_ planTokenizer: PlanTokenizing) { 51 | self.planTokenizer = planTokenizer 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/unit/RuntimeTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | // Simple instantiable target with a mutable boolean property. 22 | class State { 23 | var boolean = false 24 | } 25 | 26 | class RuntimeTests: XCTestCase { 27 | 28 | // Verify that a plan committed to a runtime is copied. 29 | func testPlansAreCopied() { 30 | let state = State() 31 | state.boolean = false 32 | 33 | let plan = ChangeBoolean(desiredBoolean: true) 34 | 35 | let runtime = MotionRuntime() 36 | let spy = RuntimeSpy() 37 | runtime.addTracer(spy) 38 | 39 | runtime.addPlan(plan, to: state) 40 | 41 | XCTAssertEqual(spy.countOf(.didAddPlan(plan: ChangeBoolean.self, target: state)), 1) 42 | } 43 | 44 | // Verify that a plan committed to a runtime immediately executes its add(plan:) logic. 45 | func testAddPlanInvokedImmediately() { 46 | let state = State() 47 | state.boolean = false 48 | 49 | let plan = ChangeBoolean(desiredBoolean: true) 50 | 51 | let runtime = MotionRuntime() 52 | runtime.addPlan(plan, to: state) 53 | 54 | XCTAssertEqual(state.boolean, plan.desiredBoolean) 55 | } 56 | 57 | private class ChangeBoolean: NSObject, Plan { 58 | var desiredBoolean: Bool 59 | 60 | init(desiredBoolean: Bool) { 61 | self.desiredBoolean = desiredBoolean 62 | } 63 | 64 | func performerClass() -> AnyClass { 65 | return Performer.self 66 | } 67 | 68 | public func copy(with zone: NSZone? = nil) -> Any { 69 | return ChangeBoolean(desiredBoolean: desiredBoolean) 70 | } 71 | 72 | private class Performer: NSObject, Performing { 73 | let target: State 74 | required init(target: Any) { 75 | self.target = target as! State 76 | } 77 | 78 | func addPlan(_ plan: Plan) { 79 | let testPlan = plan as! ChangeBoolean 80 | target.boolean = testPlan.desiredBoolean 81 | } 82 | } 83 | } 84 | 85 | // Verify that two plans of the same type creates only one performer. 86 | func testTwoSamePlansCreatesOnePerformer() { 87 | let state = State() 88 | state.boolean = false 89 | 90 | let runtime = MotionRuntime() 91 | let spy = RuntimeSpy() 92 | runtime.addTracer(spy) 93 | 94 | runtime.addPlans([ChangeBoolean(desiredBoolean: true), 95 | ChangeBoolean(desiredBoolean: false)], to: state) 96 | 97 | XCTAssertEqual(spy.countOf(.didCreatePerformer(target: state)), 1) 98 | } 99 | 100 | // Verify that two plans of different types creates two performers. 101 | func testTwoDifferentPlansCreatesTwoPerformers() { 102 | let state = State() 103 | state.boolean = false 104 | 105 | let runtime = MotionRuntime() 106 | let spy = RuntimeSpy() 107 | runtime.addTracer(spy) 108 | 109 | runtime.addPlans([ChangeBoolean(desiredBoolean: true), 110 | InstantlyInactive()], to: state) 111 | 112 | XCTAssertEqual(spy.countOf(.didCreatePerformer(target: state)), 2) 113 | } 114 | 115 | // Verify that order of plans is respected in a runtime. 116 | func testTwoPlansOrderIsRespected() { 117 | let state = State() 118 | state.boolean = false 119 | 120 | let runtime = MotionRuntime() 121 | 122 | runtime.addPlans([ChangeBoolean(desiredBoolean: true), 123 | ChangeBoolean(desiredBoolean: false)], to: state) 124 | 125 | XCTAssertEqual(state.boolean, false) 126 | 127 | runtime.addPlans([ChangeBoolean(desiredBoolean: false), 128 | ChangeBoolean(desiredBoolean: true)], to: state) 129 | 130 | XCTAssertEqual(state.boolean, true) 131 | } 132 | 133 | func testRuntimeIsDeallocatedWhenNotReferenced() { 134 | var runtime: MotionRuntime? = MotionRuntime() 135 | weak var weakRuntime: MotionRuntime? = runtime 136 | 137 | autoreleasepool { 138 | runtime!.addPlan(InstantlyInactive(), to: NSObject()) 139 | 140 | // Remove our only strong reference to the runtime. 141 | runtime = nil 142 | } 143 | 144 | // If this fails it means there's a retain cycle within the runtime somewhere. Place a 145 | // breakpoint here and use the Debug Memory Graph tool to debug. 146 | XCTAssertNil(weakRuntime) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tests/unit/TimelineTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | import Foundation 19 | import MaterialMotionRuntime 20 | 21 | class TimelineTests: XCTestCase { 22 | 23 | func testScrubberAttachementSendsEvents() { 24 | let timeline = Timeline() 25 | 26 | let spy = TimelineSpy() 27 | timeline.addObserver(spy) 28 | 29 | timeline.attachScrubber(withTimeOffset: 0) 30 | timeline.detachScrubber() 31 | 32 | XCTAssert(spy.events == [.didAttach, .didDetach]) 33 | } 34 | 35 | func testRemovedObserverReceivesNoEvents() { 36 | let timeline = Timeline() 37 | 38 | let spy = TimelineSpy() 39 | timeline.addObserver(spy) 40 | 41 | timeline.attachScrubber(withTimeOffset: 0) 42 | timeline.removeObserver(spy) 43 | timeline.detachScrubber() 44 | 45 | XCTAssert(spy.events == [.didAttach]) 46 | } 47 | 48 | func testScrubberReAttachementSendsScrubEvent() { 49 | let timeline = Timeline() 50 | 51 | let spy = TimelineSpy() 52 | timeline.addObserver(spy) 53 | 54 | timeline.attachScrubber(withTimeOffset: 0) 55 | timeline.attachScrubber(withTimeOffset: 0.5) 56 | 57 | XCTAssert(spy.events == [.didAttach, .didScrub(timeOffset: 0.5)]) 58 | } 59 | 60 | func testScrubberDetachAndReAttachementSendsScrubEvent() { 61 | let timeline = Timeline() 62 | 63 | let spy = TimelineSpy() 64 | timeline.addObserver(spy) 65 | 66 | timeline.attachScrubber(withTimeOffset: 0) 67 | timeline.detachScrubber() 68 | timeline.attachScrubber(withTimeOffset: 0.5) 69 | 70 | XCTAssert(spy.events == [.didAttach, .didDetach, .didAttach, .didScrub(timeOffset: 0.5)]) 71 | } 72 | 73 | func testScrubberDetachAndReAttachementSameValueSendsNoScrubEvent() { 74 | let timeline = Timeline() 75 | 76 | let spy = TimelineSpy() 77 | timeline.addObserver(spy) 78 | 79 | timeline.attachScrubber(withTimeOffset: 0) 80 | timeline.detachScrubber() 81 | timeline.attachScrubber(withTimeOffset: 0) 82 | 83 | XCTAssert(spy.events == [.didAttach, .didDetach, .didAttach]) 84 | } 85 | 86 | func testAttachedScrubberChangesSendsEvents() { 87 | let timeline = Timeline() 88 | 89 | timeline.attachScrubber(withTimeOffset: 0) 90 | let scrubber = timeline.scrubber! 91 | 92 | let spy = TimelineSpy() 93 | timeline.addObserver(spy) 94 | 95 | scrubber.timeOffset = 10 96 | scrubber.timeOffset = 0 97 | scrubber.timeOffset = 0.5 98 | 99 | XCTAssert(spy.events == [.didScrub(timeOffset: 10), 100 | .didScrub(timeOffset: 0), 101 | .didScrub(timeOffset: 0.5)]) 102 | } 103 | 104 | func testAttachedScrubberRepeatedChangeSendsNoEvents() { 105 | let timeline = Timeline() 106 | 107 | timeline.attachScrubber(withTimeOffset: 0) 108 | let scrubber = timeline.scrubber! 109 | 110 | let spy = TimelineSpy() 111 | timeline.addObserver(spy) 112 | 113 | scrubber.timeOffset = 10 114 | scrubber.timeOffset = 0 115 | scrubber.timeOffset = 0.5 116 | scrubber.timeOffset = 0.5 117 | 118 | XCTAssert(spy.events == [.didScrub(timeOffset: 10), 119 | .didScrub(timeOffset: 0), 120 | .didScrub(timeOffset: 0.5)]) 121 | } 122 | 123 | func testDetachedScrubberSendsNoEvents() { 124 | let timeline = Timeline() 125 | 126 | timeline.attachScrubber(withTimeOffset: 0) 127 | let scrubber = timeline.scrubber! 128 | timeline.detachScrubber() 129 | 130 | let spy = TimelineSpy() 131 | timeline.addObserver(spy) 132 | 133 | scrubber.timeOffset = 10 134 | scrubber.timeOffset = 0 135 | scrubber.timeOffset = 0.5 136 | 137 | XCTAssert(spy.events == []) 138 | } 139 | 140 | func testBeginTimeIsNonnullAfterBegin() { 141 | let timeline = Timeline() 142 | XCTAssertNil(timeline.beginTime) 143 | timeline.begin() 144 | XCTAssertNotNil(timeline.beginTime) 145 | } 146 | } 147 | --------------------------------------------------------------------------------