├── Cartfile ├── Cartfile.resolved ├── script ├── schemes.awk ├── xctool.awk ├── bootstrap ├── LICENSE.md ├── README.md └── cibuild ├── SimRecorder.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── SimRecorder.xcworkspace └── contents.xcworkspacedata ├── SimRecorder.framework └── SimRecorder.framework │ ├── SimRecorder.framework.h │ └── Info.plist ├── Makefile ├── README.md ├── SimRecorder ├── Components.plist ├── Info.plist └── main.swift └── .gitignore /Cartfile: -------------------------------------------------------------------------------- 1 | github "nomothetis/OptionKit" ~> 1.0.0 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "nomothetis/OptionKit" "1.0.0" 2 | -------------------------------------------------------------------------------- /script/schemes.awk: -------------------------------------------------------------------------------- 1 | BEGIN { 2 | FS = "\n"; 3 | } 4 | 5 | /Schemes:/ { 6 | while (getline && $0 != "") { 7 | sub(/^ +/, ""); 8 | print "'" $0 "'"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SimRecorder.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SimRecorder.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /script/xctool.awk: -------------------------------------------------------------------------------- 1 | # Exit statuses: 2 | # 3 | # 0 - No errors found. 4 | # 1 - Wrong SDK. Retry with SDK `iphonesimulator`. 5 | # 2 - Missing target. 6 | 7 | BEGIN { 8 | status = 0; 9 | } 10 | 11 | { 12 | print; 13 | } 14 | 15 | /Testing with the '(.+)' SDK is not yet supported/ { 16 | status = 1; 17 | } 18 | 19 | /does not contain a target named/ { 20 | status = 2; 21 | } 22 | 23 | END { 24 | exit status; 25 | } 26 | -------------------------------------------------------------------------------- /SimRecorder.framework/SimRecorder.framework/SimRecorder.framework.h: -------------------------------------------------------------------------------- 1 | // 2 | // SimRecorder.framework.h 3 | // SimRecorder.framework 4 | // 5 | // Created by koki-miki on 11/7/15. 6 | // Copyright © 2015 giginet. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SimRecorder.framework. 12 | FOUNDATION_EXPORT double SimRecorder_frameworkVersionNumber; 13 | 14 | //! Project version string for SimRecorder.framework. 15 | FOUNDATION_EXPORT const unsigned char SimRecorder_frameworkVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SWIFT_FILE?=SimRecorder/main.swift 2 | CARTAGE_PATH?=Carthage/Build/Mac 3 | FRAMEWORK_PATH?=@executable_path/../Frameworks/ 4 | EXECUTABLE_PATH?=build/simrec 5 | TARGET_SDK?=macosx 6 | BUILD_TOOL?=xcrun 7 | BUILD_OPTION?=-sdk $(TARGET_SDK) swiftc $(SWIFT_FILE) -F$(CARTAGE_PATH) -Xlinker -rpath -Xlinker "$(FRAMEWORK_PATH)" -o $(EXECUTABLE_PATH) 8 | 9 | bootstrap: 10 | carthage update 11 | 12 | build: bootstrap 13 | mkdir -p build 14 | $(BUILD_TOOL) $(BUILD_OPTION) 15 | 16 | clean: 17 | rm $(EXECUTABLE_PATH) 18 | 19 | prefix_install: build 20 | mkdir -p "$(PREFIX)/Frameworks" "$(PREFIX)/bin" 21 | cp -rf $(CARTAGE_PATH)/OptionKit.framework "$(PREFIX)/Frameworks/" 22 | cp build/simrec "$(PREFIX)/bin/" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Sim Recorder 2 | 3 | ## Install 4 | 5 | 6 | ``` 7 | $ brew install carthage 8 | $ brew tap giginet/utils 9 | $ brew install simrecorder 10 | ``` 11 | 12 | ## Usage 13 | 14 | 1. Launch your iOS Simulator 15 | 2. Exec `simrec` 16 | 3. Stop recording with `Ctrl-C` 17 | 18 | ``` 19 | $ simrec 20 | ``` 21 | 22 | You'd like to stop recording, you send INTERRUPT signal(`Ctrl + C`), then an animation will be generated. 23 | 24 | ### Command Line Options 25 | 26 | |Option|Description|Default| 27 | |------|-----------|-------| 28 | |-q |Quality of animation(0...1.0) |1.0| 29 | |-f |Frame rate of animation |5| 30 | |-o |Output path|animation.gif| 31 | |-l |loop count of animation|0(Infinity)| 32 | -------------------------------------------------------------------------------- /SimRecorder/Components.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BundleIsVersionChecked 7 | 8 | BundleOverwriteAction 9 | upgrade 10 | ChildBundles 11 | 12 | 13 | BundleOverwriteAction 14 | 15 | RootRelativeBundlePath 16 | Library/Frameworks/SimRecorder.framework/Versions/A/Frameworks/OptionKit.framework 17 | 18 | 19 | RootRelativeBundlePath 20 | Library/Frameworks/SimRecorder.framework 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPT_DIR=$(dirname "$0") 4 | 5 | ## 6 | ## Bootstrap Process 7 | ## 8 | 9 | main () 10 | { 11 | local submodules=$(git submodule status) 12 | local result=$? 13 | 14 | if [ "$result" -ne "0" ] 15 | then 16 | exit $result 17 | fi 18 | 19 | if [ -n "$submodules" ] 20 | then 21 | echo "*** Updating submodules..." 22 | update_submodules 23 | fi 24 | } 25 | 26 | bootstrap_submodule () 27 | { 28 | local bootstrap="script/bootstrap" 29 | 30 | if [ -e "$bootstrap" ] 31 | then 32 | echo "*** Bootstrapping $name..." 33 | "$bootstrap" >/dev/null 34 | else 35 | update_submodules 36 | fi 37 | } 38 | 39 | update_submodules () 40 | { 41 | git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule 42 | } 43 | 44 | export -f bootstrap_submodule 45 | export -f update_submodules 46 | 47 | main 48 | -------------------------------------------------------------------------------- /SimRecorder.framework/SimRecorder.framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 giginet. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /script/LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2013 Justin Spahr-Summers** 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /SimRecorder/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015 giginet. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/95bdb4e35df6f7b736f2e95dd53ef11f2a8a439e/Swift.gitignore 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.xcuserstate 32 | *.xcscmblueprint 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | 42 | # Carthage 43 | # 44 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 45 | Carthage/Checkouts 46 | Carthage/Build 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | 53 | fastlane/report.xml 54 | fastlane/screenshots 55 | 56 | build/ 57 | *.gif 58 | -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | # objc-build-scripts 2 | 3 | This project is a collection of scripts created with two goals: 4 | 5 | 1. To standardize how Objective-C projects are bootstrapped after cloning 6 | 1. To easily build Objective-C projects on continuous integration servers 7 | 8 | ## Scripts 9 | 10 | Right now, there are two important scripts: [`bootstrap`](#bootstrap) and 11 | [`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and 12 | eliminate pesky system configuration issues (like setting up a working Ruby 13 | environment). 14 | 15 | The structure of the scripts on disk is meant to follow that of a typical Ruby 16 | project: 17 | 18 | ``` 19 | script/ 20 | bootstrap 21 | cibuild 22 | ``` 23 | 24 | ### bootstrap 25 | 26 | This script is responsible for bootstrapping (initializing) your project after 27 | it's been checked out. Here, you should install or clone any dependencies that 28 | are required for a working build and development environment. 29 | 30 | By default, the script will verify that [xctool][] is installed, then initialize 31 | and update submodules recursively. If any submodules contain `script/bootstrap`, 32 | that will be run as well. 33 | 34 | To check that other tools are installed, you can set the `REQUIRED_TOOLS` 35 | environment variable before running `script/bootstrap`, or edit it within the 36 | script directly. Note that no installation is performed automatically, though 37 | this can always be added within your specific project. 38 | 39 | ### cibuild 40 | 41 | This script is responsible for building the project, as you would want it built 42 | for continuous integration. This is preferable to putting the logic on the CI 43 | server itself, since it ensures that any changes are versioned along with the 44 | source. 45 | 46 | By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode 47 | workspace or project in the working directory, then build all targets/schemes 48 | (as found by `xcodebuild -list`) using [xctool][]. 49 | 50 | You can also specify the schemes to build by passing them into the script: 51 | 52 | ```sh 53 | script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS 54 | ``` 55 | 56 | As with the `bootstrap` script, there are several environment variables that can 57 | be used to customize behavior. They can be set on the command line before 58 | invoking the script, or the defaults changed within the script directly. 59 | 60 | ## Getting Started 61 | 62 | To add the scripts to your project, read the contents of this repository into 63 | a `script` folder: 64 | 65 | ``` 66 | $ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git 67 | $ git fetch objc-build-scripts 68 | $ git read-tree --prefix=script/ -u objc-build-scripts/master 69 | ``` 70 | 71 | Then commit the changes, to incorporate the scripts into your own repository's 72 | history. You can also freely tweak the scripts for your specific project's 73 | needs. 74 | 75 | To merge in upstream changes later: 76 | 77 | ``` 78 | $ git fetch -p objc-build-scripts 79 | $ git merge --ff --squash -Xsubtree=script objc-build-scripts/master 80 | ``` 81 | 82 | [xctool]: https://github.com/facebook/xctool 83 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPT_DIR=$(dirname "$0") 4 | 5 | ## 6 | ## Configuration Variables 7 | ## 8 | 9 | SCHEMES="$@" 10 | 11 | config () 12 | { 13 | # The workspace to build. 14 | # 15 | # If not set and no workspace is found, the -workspace flag will not be passed 16 | # to `xctool`. 17 | # 18 | # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will 19 | # take precedence. 20 | : ${XCWORKSPACE=$(find_pattern "*.xcworkspace")} 21 | 22 | # The project to build. 23 | # 24 | # If not set and no project is found, the -project flag will not be passed 25 | # to `xctool`. 26 | # 27 | # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will 28 | # take precedence. 29 | : ${XCODEPROJ=$(find_pattern "*.xcodeproj")} 30 | 31 | # A bootstrap script to run before building. 32 | # 33 | # If this file does not exist, it is not considered an error. 34 | : ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"} 35 | 36 | # Extra options to pass to xctool. 37 | : ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"} 38 | 39 | # A whitespace-separated list of default schemes to build. 40 | # 41 | # Individual names can be quoted to avoid word splitting. 42 | : ${SCHEMES:=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk -f "$SCRIPT_DIR/schemes.awk")} 43 | 44 | # A whitespace-separated list of executables that must be present and locatable. 45 | : ${REQUIRED_TOOLS="xctool"} 46 | 47 | export XCWORKSPACE 48 | export XCODEPROJ 49 | export BOOTSTRAP 50 | export XCTOOL_OPTIONS 51 | export SCHEMES 52 | export REQUIRED_TOOLS 53 | } 54 | 55 | ## 56 | ## Build Process 57 | ## 58 | 59 | main () 60 | { 61 | config 62 | 63 | if [ -n "$REQUIRED_TOOLS" ] 64 | then 65 | echo "*** Checking dependencies..." 66 | check_deps 67 | fi 68 | 69 | if [ -f "$BOOTSTRAP" ] 70 | then 71 | echo "*** Bootstrapping..." 72 | "$BOOTSTRAP" || exit $? 73 | fi 74 | 75 | echo "*** The following schemes will be built:" 76 | echo "$SCHEMES" | xargs -n 1 echo " " 77 | echo 78 | 79 | echo "$SCHEMES" | xargs -n 1 | ( 80 | local status=0 81 | 82 | while read scheme 83 | do 84 | build_scheme "$scheme" || status=1 85 | done 86 | 87 | exit $status 88 | ) 89 | } 90 | 91 | check_deps () 92 | { 93 | for tool in $REQUIRED_TOOLS 94 | do 95 | which -s "$tool" 96 | if [ "$?" -ne "0" ] 97 | then 98 | echo "*** Error: $tool not found. Please install it and cibuild again." 99 | exit 1 100 | fi 101 | done 102 | } 103 | 104 | find_pattern () 105 | { 106 | ls -d $1 2>/dev/null | head -n 1 107 | } 108 | 109 | run_xctool () 110 | { 111 | if [ -n "$XCWORKSPACE" ] 112 | then 113 | xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1 114 | elif [ -n "$XCODEPROJ" ] 115 | then 116 | xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1 117 | else 118 | echo "*** No workspace or project file found." 119 | exit 1 120 | fi 121 | } 122 | 123 | parse_build () 124 | { 125 | awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null 126 | } 127 | 128 | build_scheme () 129 | { 130 | local scheme=$1 131 | 132 | echo "*** Building and testing $scheme..." 133 | echo 134 | 135 | local sdkflag= 136 | local action=test 137 | 138 | # Determine whether we can run unit tests for this target. 139 | run_xctool -scheme "$scheme" run-tests | parse_build 140 | 141 | local awkstatus=$? 142 | 143 | if [ "$awkstatus" -eq "1" ] 144 | then 145 | # SDK not found, try for iphonesimulator. 146 | sdkflag="-sdk iphonesimulator" 147 | 148 | # Determine whether the unit tests will run with iphonesimulator 149 | run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build 150 | 151 | awkstatus=$? 152 | 153 | if [ "$awkstatus" -ne "0" ] 154 | then 155 | # Unit tests will not run on iphonesimulator. 156 | sdkflag="" 157 | fi 158 | fi 159 | 160 | if [ "$awkstatus" -ne "0" ] 161 | then 162 | # Unit tests aren't supported. 163 | action=build 164 | fi 165 | 166 | run_xctool $sdkflag -scheme "$scheme" $action 167 | } 168 | 169 | export -f build_scheme 170 | export -f run_xctool 171 | export -f parse_build 172 | 173 | main 174 | -------------------------------------------------------------------------------- /SimRecorder.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 54210D771BEB88C10018FC44 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54210D761BEB88C10018FC44 /* main.swift */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 54210D661BEB887C0018FC44 /* SimRecorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimRecorder.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | 54210D701BEB887C0018FC44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 16 | 54210D761BEB88C10018FC44 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 17 | /* End PBXFileReference section */ 18 | 19 | /* Begin PBXFrameworksBuildPhase section */ 20 | 54210D631BEB887C0018FC44 /* Frameworks */ = { 21 | isa = PBXFrameworksBuildPhase; 22 | buildActionMask = 2147483647; 23 | files = ( 24 | ); 25 | runOnlyForDeploymentPostprocessing = 0; 26 | }; 27 | /* End PBXFrameworksBuildPhase section */ 28 | 29 | /* Begin PBXGroup section */ 30 | 54210D5D1BEB887C0018FC44 = { 31 | isa = PBXGroup; 32 | children = ( 33 | 54210D681BEB887C0018FC44 /* SimRecorder */, 34 | 54210D671BEB887C0018FC44 /* Products */, 35 | ); 36 | sourceTree = ""; 37 | }; 38 | 54210D671BEB887C0018FC44 /* Products */ = { 39 | isa = PBXGroup; 40 | children = ( 41 | 54210D661BEB887C0018FC44 /* SimRecorder.app */, 42 | ); 43 | name = Products; 44 | sourceTree = ""; 45 | }; 46 | 54210D681BEB887C0018FC44 /* SimRecorder */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 54210D701BEB887C0018FC44 /* Info.plist */, 50 | 54210D761BEB88C10018FC44 /* main.swift */, 51 | ); 52 | path = SimRecorder; 53 | sourceTree = ""; 54 | }; 55 | /* End PBXGroup section */ 56 | 57 | /* Begin PBXNativeTarget section */ 58 | 54210D651BEB887C0018FC44 /* SimRecorder */ = { 59 | isa = PBXNativeTarget; 60 | buildConfigurationList = 54210D731BEB887C0018FC44 /* Build configuration list for PBXNativeTarget "SimRecorder" */; 61 | buildPhases = ( 62 | 54210D621BEB887C0018FC44 /* Sources */, 63 | 54210D631BEB887C0018FC44 /* Frameworks */, 64 | 54210D641BEB887C0018FC44 /* Resources */, 65 | ); 66 | buildRules = ( 67 | ); 68 | dependencies = ( 69 | ); 70 | name = SimRecorder; 71 | productName = SimRecorder; 72 | productReference = 54210D661BEB887C0018FC44 /* SimRecorder.app */; 73 | productType = "com.apple.product-type.application"; 74 | }; 75 | /* End PBXNativeTarget section */ 76 | 77 | /* Begin PBXProject section */ 78 | 54210D5E1BEB887C0018FC44 /* Project object */ = { 79 | isa = PBXProject; 80 | attributes = { 81 | LastSwiftUpdateCheck = 0710; 82 | LastUpgradeCheck = 0710; 83 | ORGANIZATIONNAME = giginet; 84 | TargetAttributes = { 85 | 54210D651BEB887C0018FC44 = { 86 | CreatedOnToolsVersion = 7.1; 87 | }; 88 | }; 89 | }; 90 | buildConfigurationList = 54210D611BEB887C0018FC44 /* Build configuration list for PBXProject "SimRecorder" */; 91 | compatibilityVersion = "Xcode 3.2"; 92 | developmentRegion = English; 93 | hasScannedForEncodings = 0; 94 | knownRegions = ( 95 | en, 96 | Base, 97 | ); 98 | mainGroup = 54210D5D1BEB887C0018FC44; 99 | productRefGroup = 54210D671BEB887C0018FC44 /* Products */; 100 | projectDirPath = ""; 101 | projectRoot = ""; 102 | targets = ( 103 | 54210D651BEB887C0018FC44 /* SimRecorder */, 104 | ); 105 | }; 106 | /* End PBXProject section */ 107 | 108 | /* Begin PBXResourcesBuildPhase section */ 109 | 54210D641BEB887C0018FC44 /* Resources */ = { 110 | isa = PBXResourcesBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | ); 114 | runOnlyForDeploymentPostprocessing = 0; 115 | }; 116 | /* End PBXResourcesBuildPhase section */ 117 | 118 | /* Begin PBXSourcesBuildPhase section */ 119 | 54210D621BEB887C0018FC44 /* Sources */ = { 120 | isa = PBXSourcesBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | 54210D771BEB88C10018FC44 /* main.swift in Sources */, 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | /* End PBXSourcesBuildPhase section */ 128 | 129 | /* Begin XCBuildConfiguration section */ 130 | 54210D711BEB887C0018FC44 /* Debug */ = { 131 | isa = XCBuildConfiguration; 132 | buildSettings = { 133 | ALWAYS_SEARCH_USER_PATHS = NO; 134 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 135 | CLANG_CXX_LIBRARY = "libc++"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_WARN_BOOL_CONVERSION = YES; 139 | CLANG_WARN_CONSTANT_CONVERSION = YES; 140 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 141 | CLANG_WARN_EMPTY_BODY = YES; 142 | CLANG_WARN_ENUM_CONVERSION = YES; 143 | CLANG_WARN_INT_CONVERSION = YES; 144 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 145 | CLANG_WARN_UNREACHABLE_CODE = YES; 146 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 147 | CODE_SIGN_IDENTITY = "-"; 148 | COPY_PHASE_STRIP = NO; 149 | DEBUG_INFORMATION_FORMAT = dwarf; 150 | ENABLE_STRICT_OBJC_MSGSEND = YES; 151 | ENABLE_TESTABILITY = YES; 152 | GCC_C_LANGUAGE_STANDARD = gnu99; 153 | GCC_DYNAMIC_NO_PIC = NO; 154 | GCC_NO_COMMON_BLOCKS = YES; 155 | GCC_OPTIMIZATION_LEVEL = 0; 156 | GCC_PREPROCESSOR_DEFINITIONS = ( 157 | "DEBUG=1", 158 | "$(inherited)", 159 | ); 160 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 161 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 162 | GCC_WARN_UNDECLARED_SELECTOR = YES; 163 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 164 | GCC_WARN_UNUSED_FUNCTION = YES; 165 | GCC_WARN_UNUSED_VARIABLE = YES; 166 | MACOSX_DEPLOYMENT_TARGET = 10.10; 167 | MTL_ENABLE_DEBUG_INFO = YES; 168 | ONLY_ACTIVE_ARCH = YES; 169 | SDKROOT = macosx; 170 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 171 | }; 172 | name = Debug; 173 | }; 174 | 54210D721BEB887C0018FC44 /* Release */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 179 | CLANG_CXX_LIBRARY = "libc++"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_WARN_BOOL_CONVERSION = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_EMPTY_BODY = YES; 186 | CLANG_WARN_ENUM_CONVERSION = YES; 187 | CLANG_WARN_INT_CONVERSION = YES; 188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 189 | CLANG_WARN_UNREACHABLE_CODE = YES; 190 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 191 | CODE_SIGN_IDENTITY = "-"; 192 | COPY_PHASE_STRIP = NO; 193 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 194 | ENABLE_NS_ASSERTIONS = NO; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | GCC_C_LANGUAGE_STANDARD = gnu99; 197 | GCC_NO_COMMON_BLOCKS = YES; 198 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 199 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 200 | GCC_WARN_UNDECLARED_SELECTOR = YES; 201 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 202 | GCC_WARN_UNUSED_FUNCTION = YES; 203 | GCC_WARN_UNUSED_VARIABLE = YES; 204 | MACOSX_DEPLOYMENT_TARGET = 10.10; 205 | MTL_ENABLE_DEBUG_INFO = NO; 206 | SDKROOT = macosx; 207 | }; 208 | name = Release; 209 | }; 210 | 54210D741BEB887C0018FC44 /* Debug */ = { 211 | isa = XCBuildConfiguration; 212 | buildSettings = { 213 | CLANG_ENABLE_MODULES = YES; 214 | COMBINE_HIDPI_IMAGES = YES; 215 | INFOPLIST_FILE = SimRecorder/Info.plist; 216 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 217 | PRODUCT_BUNDLE_IDENTIFIER = "net.gigi-net.SimRecorder"; 218 | PRODUCT_NAME = "$(TARGET_NAME)"; 219 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 220 | }; 221 | name = Debug; 222 | }; 223 | 54210D751BEB887C0018FC44 /* Release */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | CLANG_ENABLE_MODULES = YES; 227 | COMBINE_HIDPI_IMAGES = YES; 228 | INFOPLIST_FILE = SimRecorder/Info.plist; 229 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 230 | PRODUCT_BUNDLE_IDENTIFIER = "net.gigi-net.SimRecorder"; 231 | PRODUCT_NAME = "$(TARGET_NAME)"; 232 | }; 233 | name = Release; 234 | }; 235 | /* End XCBuildConfiguration section */ 236 | 237 | /* Begin XCConfigurationList section */ 238 | 54210D611BEB887C0018FC44 /* Build configuration list for PBXProject "SimRecorder" */ = { 239 | isa = XCConfigurationList; 240 | buildConfigurations = ( 241 | 54210D711BEB887C0018FC44 /* Debug */, 242 | 54210D721BEB887C0018FC44 /* Release */, 243 | ); 244 | defaultConfigurationIsVisible = 0; 245 | defaultConfigurationName = Release; 246 | }; 247 | 54210D731BEB887C0018FC44 /* Build configuration list for PBXNativeTarget "SimRecorder" */ = { 248 | isa = XCConfigurationList; 249 | buildConfigurations = ( 250 | 54210D741BEB887C0018FC44 /* Debug */, 251 | 54210D751BEB887C0018FC44 /* Release */, 252 | ); 253 | defaultConfigurationIsVisible = 0; 254 | defaultConfigurationName = Release; 255 | }; 256 | /* End XCConfigurationList section */ 257 | }; 258 | rootObject = 54210D5E1BEB887C0018FC44 /* Project object */; 259 | } 260 | -------------------------------------------------------------------------------- /SimRecorder/main.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift -framework OptionKit -F Carthage/Build/Mac 2 | import Cocoa 3 | import CoreGraphics 4 | import OptionKit 5 | 6 | extension NSImage { 7 | var CGImage: CGImageRef { 8 | get { 9 | let imageData = self.TIFFRepresentation 10 | let source = CGImageSourceCreateWithData(imageData!, nil) 11 | let maskRef = CGImageSourceCreateImageAtIndex(source!, 0, nil)! 12 | return maskRef; 13 | } 14 | } 15 | } 16 | 17 | enum TargetSimulator: String { 18 | case iOS = "ios" 19 | case iOSWatch = "ios-watch" 20 | case Android64ARM = "android64-arm" 21 | case Android64X86 = "android64-x86" 22 | case Android64MIPS = "android64-mips" 23 | 24 | func hasBundleId() -> Bool { 25 | return self == .iOS || self == .iOSWatch 26 | } 27 | 28 | func toFilterValue() -> String { 29 | switch self { 30 | case .iOS: return "com.apple.iphonesimulator" 31 | case .iOSWatch: return "com.apple.watchsimulator" 32 | case .Android64ARM: return "emulator64-arm" 33 | case .Android64X86: return "emulator64-x86" 34 | case .Android64MIPS: return "emulator64-mips" 35 | } 36 | } 37 | } 38 | 39 | class Storage { 40 | private let DefaultTempDirName = "simrec" 41 | private var url : NSURL? 42 | 43 | init() { 44 | self.url = createTemporaryDirectory() 45 | } 46 | 47 | func createTemporaryDirectory() -> NSURL? { 48 | let url : NSURL = NSURL(fileURLWithPath: NSTemporaryDirectory()) 49 | let pathURL : NSURL = url.URLByAppendingPathComponent(DefaultTempDirName) 50 | let fileManager = NSFileManager.defaultManager() 51 | do { 52 | try fileManager.createDirectoryAtURL(pathURL, withIntermediateDirectories: true, attributes: nil) 53 | return pathURL 54 | } catch { 55 | return nil 56 | } 57 | } 58 | 59 | func basePath() -> String? { 60 | return self.url?.absoluteString; 61 | } 62 | 63 | func writeToFile(image : CGImageRef, filename : String) -> NSData { 64 | let bitmapRep : NSBitmapImageRep = NSBitmapImageRep(CGImage: image) 65 | let fileURL : NSURL = NSURL(string: filename, relativeToURL: self.url)! 66 | let properties = Dictionary() 67 | let data : NSData = bitmapRep.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: properties)! 68 | if !data.writeToFile(fileURL.path!, atomically: false) { 69 | print("write to file failed") 70 | } 71 | return data 72 | } 73 | } 74 | 75 | class Converter { 76 | typealias ConvertFinishedCallback = (data: NSData?, succeed: Bool) -> () 77 | 78 | func createGIF(with images: [NSImage], quality: Float = 1.0, loopCount: UInt = 0, frameDelay: Double, destinationURL : NSURL, callback : ConvertFinishedCallback?) { 79 | let frameCount = images.count 80 | let animationProperties = 81 | [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: loopCount], 82 | kCGImageDestinationLossyCompressionQuality as String: quality] 83 | let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: frameDelay, kCGImagePropertyGIFUnclampedDelayTime as String: frameDelay]] 84 | 85 | let destination = CGImageDestinationCreateWithURL(destinationURL, kUTTypeGIF, frameCount, nil) 86 | if let destination = destination { 87 | CGImageDestinationSetProperties(destination, animationProperties) 88 | for var i = 0; i < frameCount; ++i { 89 | autoreleasepool { 90 | let image = images[i]; 91 | CGImageDestinationAddImage(destination, image.CGImage, frameProperties) 92 | } 93 | } 94 | if CGImageDestinationFinalize(destination) { 95 | if let callback = callback { 96 | callback(data: NSData(contentsOfURL: destinationURL), succeed: true) 97 | } 98 | } else { 99 | if let callback = callback { 100 | callback(data: nil, succeed: false) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | class Recorder { 108 | private var windowID : CGWindowID? 109 | private var frame: UInt = 0 110 | private var timer: NSTimer! 111 | private let storage: Storage = Storage() 112 | private let converter: Converter = Converter() 113 | private var images: [NSImage] = [] 114 | var quality: Float = 1.0 115 | var fps: UInt = 5 116 | var outputPath: String = "animation.gif" 117 | var loopCount: UInt = 0 118 | var targetSimulator: TargetSimulator 119 | 120 | convenience init() { 121 | self.init(targetSimulator: .iOS) 122 | } 123 | 124 | init(targetSimulator: TargetSimulator) { 125 | self.targetSimulator = targetSimulator 126 | self.windowID = self.simulatorWindowID() 127 | } 128 | 129 | private func simulatorWindowID() -> CGWindowID? { 130 | var windowIDs : [CGWindowID] = [] 131 | 132 | let simulators : [NSRunningApplication] 133 | if targetSimulator.hasBundleId() { 134 | simulators = NSWorkspace.sharedWorkspace().runningApplications.filter({ 135 | (app : NSRunningApplication) in 136 | return app.bundleIdentifier == targetSimulator.toFilterValue() 137 | }) 138 | } else { 139 | simulators = NSWorkspace.sharedWorkspace().runningApplications.filter({ 140 | (app : NSRunningApplication) in 141 | return app.localizedName == targetSimulator.toFilterValue() 142 | }) 143 | } 144 | 145 | if (simulators.count > 0) { 146 | let simulator : NSRunningApplication = simulators.first! 147 | 148 | let windowArray : CFArrayRef = CGWindowListCopyWindowInfo(CGWindowListOption.OptionOnScreenOnly, 0)! 149 | let windows : NSArray = windowArray as NSArray 150 | for window in windows { 151 | let dict = window as! Dictionary 152 | let windowIDNumber: NSNumber = dict["kCGWindowNumber"] as! NSNumber 153 | let ownerPID: NSNumber = dict["kCGWindowOwnerPID"] as! NSNumber 154 | if ownerPID.intValue == Int32(simulator.processIdentifier) { 155 | windowIDs.append(CGWindowID(windowIDNumber.intValue)) 156 | } 157 | } 158 | } 159 | if windowIDs.count > 0 { 160 | return windowIDs.last; 161 | } 162 | return nil 163 | } 164 | 165 | func secPerFrame() -> Double { 166 | return 1.0 / Double(self.fps) 167 | } 168 | 169 | func outputURL() -> NSURL { 170 | return NSURL(string: self.outputPath)! 171 | } 172 | 173 | func isAttachSimulator() -> Bool { 174 | return self.windowID != nil 175 | } 176 | 177 | @objc private func takeScreenshot() { 178 | let imageRef : CGImageRef = CGWindowListCreateImage(CGRectNull, CGWindowListOption.OptionIncludingWindow, windowID!, CGWindowImageOption.BoundsIgnoreFraming)! 179 | let newRef = removeAlpha(imageRef) 180 | let data : NSData = self.storage.writeToFile(newRef, filename: "\(self.frame).png") 181 | ++self.frame 182 | let image = NSImage(data: data) 183 | if let image = image { 184 | self.images.append(image) 185 | } 186 | } 187 | 188 | private func removeAlpha(imageRef: CGImageRef) -> CGImageRef { 189 | let width = CGImageGetWidth(imageRef) 190 | let height = CGImageGetHeight(imageRef) 191 | let bitmapContext: CGContextRef? = CGBitmapContextCreate(nil, 192 | width, 193 | height, 194 | CGImageGetBitsPerComponent(imageRef), 195 | CGImageGetBytesPerRow(imageRef), 196 | CGImageGetColorSpace(imageRef), 197 | CGImageAlphaInfo.NoneSkipLast.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue) 198 | let rect: CGRect = CGRectMake(0, 0, CGFloat(width), CGFloat(height)) 199 | if let bitmapContext = bitmapContext { 200 | CGContextDrawImage(bitmapContext, rect, imageRef); 201 | return CGBitmapContextCreateImage(bitmapContext)! 202 | } 203 | return imageRef 204 | } 205 | 206 | func startCapture() { 207 | self.timer = NSTimer.scheduledTimerWithTimeInterval(self.secPerFrame(), target: self, selector: "takeScreenshot", userInfo: nil, repeats: true) 208 | } 209 | 210 | func endCapture(callback : Converter.ConvertFinishedCallback?) { 211 | self.timer?.invalidate() 212 | let destinationURL : NSURL = NSURL(fileURLWithPath: self.outputPath) 213 | self.converter.createGIF(with: self.images, quality: self.quality, loopCount: self.loopCount, frameDelay: self.secPerFrame(), destinationURL: destinationURL, callback: callback) 214 | } 215 | } 216 | 217 | class Command { 218 | typealias SignalCallback = @convention(c) (Int32) -> Void 219 | 220 | static func execute(arguments : [String]) { 221 | let frameRateOption = Option(trigger: OptionTrigger.Mixed("f", "fps"), numberOfParameters: 1, helpDescription: "Recording frames per second") 222 | let outputPathOption = Option(trigger: OptionTrigger.Mixed("o", "outputPath"), numberOfParameters: 1, helpDescription: "Animation output path") 223 | let qualityOption = Option(trigger: OptionTrigger.Mixed("q", "quality"), numberOfParameters: 1, helpDescription: "Quality of animations 0.0 ~ 1.0") 224 | let loopCountOption = Option(trigger: OptionTrigger.Mixed("l", "loopCount"), numberOfParameters: 1, helpDescription: "Loop count of animations. if you passed 0, it animate eternally") 225 | let targetSimulatorOption = Option(trigger: OptionTrigger.Mixed("t", "targetSimulator"), numberOfParameters: 1, helpDescription: "Target simulator [\(TargetSimulator.iOS.rawValue)|\(TargetSimulator.Android64ARM.rawValue)|\(TargetSimulator.Android64X86.rawValue)|\(TargetSimulator.Android64MIPS.rawValue)]") 226 | 227 | let parser = OptionParser(definitions: [frameRateOption, outputPathOption, qualityOption, loopCountOption, targetSimulatorOption]) 228 | 229 | do { 230 | let (options, _) = try parser.parse(arguments) 231 | 232 | let recorder : Recorder 233 | 234 | if let targetSimulator = options[targetSimulatorOption]?.first { 235 | if let targetSimulatorEnum = TargetSimulator.init(rawValue: targetSimulator) { 236 | recorder = Recorder(targetSimulator: targetSimulatorEnum) 237 | } else { 238 | recorder = Recorder() 239 | } 240 | } else { 241 | recorder = Recorder() 242 | } 243 | 244 | guard recorder.isAttachSimulator() else { 245 | print("iOS simulator seems not to launch") 246 | exit(EXIT_FAILURE) 247 | } 248 | 249 | if let frameRate: UInt = options[frameRateOption]?.flatMap({ UInt($0) }).first { 250 | recorder.fps = frameRate 251 | } 252 | 253 | if let outputPath = options[outputPathOption]?.first { 254 | recorder.outputPath = outputPath 255 | } 256 | 257 | if let quality: Float = options[qualityOption]?.flatMap({ Float($0) }).first { 258 | recorder.quality = quality 259 | } 260 | 261 | if let loopCount: UInt = options[loopCountOption]?.flatMap({ UInt($0) }).first { 262 | recorder.loopCount = loopCount 263 | } 264 | 265 | let callback : @convention(block) (Int32) -> Void = { (Int32) -> Void in 266 | recorder.endCapture({ (data : NSData?, succeed : Bool) in 267 | if succeed { 268 | print("Gif animation generated") 269 | exit(EXIT_SUCCESS) 270 | } else { 271 | print("Gif animation generation is failed") 272 | exit(EXIT_FAILURE) 273 | } 274 | }) 275 | } 276 | 277 | // Convert Objective-C block to C function pointer 278 | let imp = imp_implementationWithBlock(unsafeBitCast(callback, AnyObject.self)) 279 | signal(SIGINT, unsafeBitCast(imp, SignalCallback.self)) 280 | recorder.startCapture() 281 | autoreleasepool { 282 | NSRunLoop.currentRunLoop().run() 283 | } 284 | } catch { 285 | } 286 | } 287 | } 288 | 289 | let actualArguments = Array(Process.arguments[1..