├── .gitignore ├── screenshot.jpg ├── ios └── FastCamera │ ├── EVNCamera │ ├── EVNCamera.bundle │ │ ├── en.lproj │ │ │ └── Root.strings │ │ └── Root.plist │ ├── CRCountdown.h │ ├── EVNCameraController.h │ ├── CRCountdown.m │ └── EVNCameraController.m │ ├── FastCameraManager.h │ ├── FastCamera.h │ ├── FastCameraManager.m │ └── FastCamera.m ├── package.json ├── license ├── yarn.lock ├── index.js ├── example.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismnoiet/react-native-fast-camera/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/EVNCamera.bundle/en.lproj/Root.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismnoiet/react-native-fast-camera/HEAD/ios/FastCamera/EVNCamera/EVNCamera.bundle/en.lproj/Root.strings -------------------------------------------------------------------------------- /ios/FastCamera/FastCameraManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | @interface FastCameraManager : RCTViewManager 8 | @end 9 | 10 | NS_ASSUME_NONNULL_END 11 | -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/CRCountdown.h: -------------------------------------------------------------------------------- 1 | // 2 | // CRCountdown.h 3 | // roaa 4 | // 5 | // Created by mac on 2019-10-07. 6 | // Copyright © 2019 Facebook. All rights reserved. 7 | // 8 | 9 | typedef void (^CRCountdownCompletion)(void); 10 | typedef void (^CRCountdownUpdate)(NSUInteger); 11 | 12 | @interface CRCountdown : NSObject 13 | 14 | - (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion update:(CRCountdownUpdate)update; 15 | @property (readonly) NSUInteger ticksRemaining; 16 | @property (readonly) NSTimeInterval interval; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-fast-camera", 3 | "version": "0.1.0", 4 | "description": "High-quality and fast camera component for react native", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "camera", 11 | "fast", 12 | "native", 13 | "ios", 14 | "android", 15 | "fast-camera", 16 | "react-component", 17 | "react-native-component", 18 | "react-native" 19 | ], 20 | "author": "ismnoiet ", 21 | "homepage": "https://github.com/ismnoiet/react-native-fast-camera", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/ismnoiet/react-native-fast-camera" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/ismnoiet/react-native-fast-camera.git" 28 | }, 29 | "license": "MIT", 30 | "dependencies": { 31 | "prop-types": "^15.7.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ios/FastCamera/FastCamera.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | 6 | #import "EVNCameraController.h" 7 | 8 | @class RCTEventDispatcher; 9 | 10 | @interface FastCamera : UIView 11 | // Define view properties here with @property 12 | // @property (nonatomic, assign) NSString* btnColor; 13 | @property (nonatomic, copy) RCTDirectEventBlock onChange; 14 | @property (nonatomic, copy) RCTDirectEventBlock onSaveSuccess; 15 | @property (nonatomic, copy) RCTDirectEventBlock onGalleryImage; 16 | @property (nonatomic, copy) RCTDirectEventBlock onFlashToggle; 17 | 18 | - (NSString *)customFormattedDate; 19 | 20 | -(void)pickImage; 21 | -(void)toggleFlash; 22 | -(void)takePicture; 23 | -(void)timer; 24 | -(void)flipCamera; 25 | 26 | // Initializing with the event dispatcher allows us to communicate with JS 27 | - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/EVNCameraController.h: -------------------------------------------------------------------------------- 1 | // 2 | // EVNCameraController.h 3 | // EVNCamera 4 | // 5 | // Created by developer on 2017/6/9. 6 | // Copyright © 2017年 仁伯安. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /** 12 | * Camera shooting agent 13 | */ 14 | @protocol EVNCameraControllerDelegate 15 | 16 | - (void)cameraDidFinishShootWithCameraImage:(UIImage *)cameraImage; 17 | - (void)didFinishPickingMediaWithInfo:(NSDictionary *)info; 18 | - (void)sendPictureWithLocation:(NSArray *) info; 19 | - (void)flashDidFinishToggle:(NSArray *) info; 20 | 21 | @end 22 | 23 | 24 | /** 25 | * Custom camera view controller 26 | */ 27 | @interface EVNCameraController : UIViewController 28 | 29 | 30 | @property (weak, nonatomic) id cameraControllerDelegate; 31 | - (void)flipMyCamera; 32 | - (void)pickImage; 33 | - (void)toggleFlash; 34 | - (void)takePicture; 35 | - (void)toggleTimer; 36 | - (void)privateToggleTimer; 37 | 38 | @end 39 | 40 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) ismnoiet (ismnoiet.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "js-tokens@^3.0.0 || ^4.0.0": 6 | version "4.0.0" 7 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 8 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 9 | 10 | loose-envify@^1.4.0: 11 | version "1.4.0" 12 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 13 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 14 | dependencies: 15 | js-tokens "^3.0.0 || ^4.0.0" 16 | 17 | object-assign@^4.1.1: 18 | version "4.1.1" 19 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 20 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 21 | 22 | prop-types@^15.7.2: 23 | version "15.7.2" 24 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" 25 | integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 26 | dependencies: 27 | loose-envify "^1.4.0" 28 | object-assign "^4.1.1" 29 | react-is "^16.8.1" 30 | 31 | react-is@^16.8.1: 32 | version "16.13.0" 33 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" 34 | integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== 35 | -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/EVNCamera.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StringsTable 6 | Root 7 | PreferenceSpecifiers 8 | 9 | 10 | Type 11 | PSGroupSpecifier 12 | Title 13 | Group 14 | 15 | 16 | Type 17 | PSTextFieldSpecifier 18 | Title 19 | Name 20 | Key 21 | name_preference 22 | DefaultValue 23 | 24 | IsSecure 25 | 26 | KeyboardType 27 | Alphabet 28 | AutocapitalizationType 29 | None 30 | AutocorrectionType 31 | No 32 | 33 | 34 | Type 35 | PSToggleSwitchSpecifier 36 | Title 37 | Enabled 38 | Key 39 | enabled_preference 40 | DefaultValue 41 | 42 | 43 | 44 | Type 45 | PSSliderSpecifier 46 | Key 47 | slider_preference 48 | DefaultValue 49 | 0.5 50 | MinimumValue 51 | 0 52 | MaximumValue 53 | 1 54 | MinimumValueImage 55 | 56 | MaximumValueImage 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ios/FastCamera/FastCameraManager.m: -------------------------------------------------------------------------------- 1 | #import "FastCameraManager.h" 2 | #import "FastCamera.h" 3 | 4 | #import 5 | #import 6 | 7 | @implementation FastCameraManager 8 | 9 | @synthesize bridge = _bridge; 10 | 11 | FastCamera *_advancedView; 12 | 13 | RCT_EXPORT_MODULE(FastCamera) 14 | 15 | - (UIView *)view 16 | { 17 | 18 | // instanciate the new cam component. 19 | _advancedView = [[FastCamera alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; 20 | // _advancedView.backgroundColor = [UIColor orangeColor]; 21 | // _advancedView.frame = CGRectMake(0, 0, 375, 667); 22 | // _advancedView.userInteractionEnabled = YES; 23 | 24 | return _advancedView; 25 | 26 | // return [[RoaaCamera alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; 27 | } 28 | 29 | 30 | RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) 31 | { 32 | RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); 33 | 34 | } 35 | 36 | RCT_EXPORT_METHOD(flipCamera) 37 | { 38 | 39 | RCTLogInfo(@"camera will be flipped"); 40 | [_advancedView flipCamera]; 41 | } 42 | 43 | RCT_EXPORT_METHOD(pickImage) 44 | { 45 | 46 | RCTLogInfo(@"pickImage"); 47 | [_advancedView pickImage]; 48 | } 49 | 50 | RCT_EXPORT_METHOD(toggleFlash) 51 | { 52 | 53 | RCTLogInfo(@"toggleFlash"); 54 | [_advancedView toggleFlash]; 55 | } 56 | 57 | RCT_EXPORT_METHOD(timer) 58 | { 59 | 60 | RCTLogInfo(@"timer"); 61 | [_advancedView timer]; 62 | } 63 | 64 | RCT_EXPORT_METHOD(takePicture) 65 | { 66 | RCTLogInfo(@"takePicture"); 67 | [_advancedView takePicture]; 68 | } 69 | 70 | RCT_EXPORT_VIEW_PROPERTY(onSaveSuccess, RCTBubblingEventBlock); 71 | RCT_EXPORT_VIEW_PROPERTY(onGalleryImage, RCTBubblingEventBlock); 72 | RCT_EXPORT_VIEW_PROPERTY(onFlashToggle, RCTBubblingEventBlock); 73 | 74 | @end 75 | 76 | -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/CRCountdown.m: -------------------------------------------------------------------------------- 1 | // 2 | // CRCountdown.m 3 | // roaa 4 | // 5 | // Created by mac on 2019-10-07. 6 | // Copyright © 2019 Facebook. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CRCountdown.h" 11 | 12 | @interface CRCountdown() 13 | 14 | @property NSTimer *timer; 15 | @property NSTimer *updateTimer; 16 | @property (readwrite) NSTimeInterval interval; 17 | @property (copy) CRCountdownCompletion completion; 18 | @property (copy) CRCountdownUpdate update; 19 | 20 | @end 21 | 22 | @implementation CRCountdown 23 | 24 | - (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion update:(CRCountdownUpdate)update { 25 | self.completion = completion; 26 | self.update = update; 27 | self.interval = interval; 28 | self.timer = [NSTimer scheduledTimerWithTimeInterval:(interval * ticks) target:self selector:@selector(countdownComplete:) userInfo:nil repeats:NO]; 29 | if (self.update) { 30 | self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(countdownUpdate:) userInfo:nil repeats:YES]; 31 | } 32 | } 33 | 34 | - (void)stopCountdown { 35 | [self.updateTimer invalidate]; 36 | [self.timer invalidate]; 37 | } 38 | 39 | - (NSUInteger)ticksRemaining { 40 | if (self.timer.isValid) { 41 | NSTimeInterval timeRemaining = [self.timer.fireDate timeIntervalSinceDate:[NSDate date]]; 42 | return timeRemaining / self.interval; 43 | } else { 44 | return 0; 45 | } 46 | } 47 | 48 | - (void)countdownUpdate:(NSTimer *)timer { 49 | if (self.update) { 50 | self.update(self.ticksRemaining); 51 | NSString *s = [NSString stringWithFormat:@"%lu", (unsigned long)self.ticksRemaining]; 52 | } 53 | } 54 | 55 | - (void)countdownComplete:(NSTimer *)timer { 56 | [self.updateTimer invalidate]; 57 | if (self.completion) { 58 | self.completion(); 59 | } 60 | 61 | self.update = nil; 62 | self.completion = nil; 63 | } 64 | 65 | @end 66 | 67 | 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | requireNativeComponent, 6 | NativeModules, 7 | Dimensions, 8 | Platform, 9 | } from 'react-native'; 10 | import PropTypes from 'prop-types'; 11 | 12 | let FastCamera, FastCameraMethods; 13 | if (Platform.OS === 'ios') { 14 | FastCamera = requireNativeComponent('FastCamera'); 15 | FastCameraMethods = NativeModules.FastCamera; 16 | } 17 | 18 | const { width, height } = Dimensions.get('window'); 19 | 20 | const androidCamera = () => ( 21 | 22 | android not supported yet! 23 | 24 | ); 25 | 26 | const iOSCamera = (props) => { 27 | console.log('props::: ', props); 28 | return ( 29 | 30 | { 33 | // console.log('onSaveSuccess: ', data.nativeEvent); 34 | props.onSaveSuccess ? props.onSaveSuccess(data.nativeEvent.image) : null; 35 | }} 36 | onGalleryImage={data => { 37 | // console.log('onGalleryImage: ', data.nativeEvent); 38 | props.onGalleryImage ? props.onGalleryImage(data.nativeEvent.image) : null; 39 | }} 40 | onFlashToggle={data => { 41 | // console.log('flash info: ', data.nativeEvent); 42 | props.onFlashToggle ? props.onFlashToggle(data.nativeEvent.isflashOn) : null; 43 | }} 44 | /> 45 | {props.children} 46 | 47 | ); 48 | } 49 | 50 | class MyFastCamera extends Component { 51 | constructor(props) { 52 | super(props); 53 | this.state = { 54 | showSelectedPictureOptions: false, // set when the user select or take a picture. 55 | imageInfo: null, 56 | modalVisible: false, 57 | }; 58 | } 59 | 60 | render() { 61 | return Platform.OS === 'ios' ? iOSCamera(this.props) : androidCamera(this.props); 62 | } 63 | } 64 | 65 | MyFastCamera.propTypes = { 66 | onSaveSuccess: PropTypes.func, 67 | onGalleryImage: PropTypes.func, 68 | onFlashToggle: PropTypes.func, 69 | }; 70 | 71 | export default MyFastCamera; 72 | 73 | export const Methods = FastCameraMethods; 74 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | * @flow 7 | */ 8 | 9 | import React from 'react'; 10 | import { 11 | StyleSheet, 12 | View, 13 | Text, 14 | TouchableOpacity, 15 | } from 'react-native'; 16 | 17 | import FastCamera, { Methods } from 'react-native-fast-camera'; 18 | 19 | const Item = (props) => ( 20 | 24 | {props.text} 25 | 26 | ); 27 | 28 | const App: () => React$Node = () => { 29 | 30 | console.log('camera component: ', FastCamera); 31 | 32 | return ( 33 | { 35 | console.log('onSaveSuccess: ', imageUrl); 36 | }} 37 | onGalleryImage={imageUrl => { 38 | console.log('onGalleryImage: ', imageUrl); 39 | }} 40 | onFlashToggle={isflashOn => { 41 | console.log('flash info: ', isflashOn); 42 | }} 43 | > 44 | 45 | 46 | { Methods.timer(); }} 48 | text="Timer" 49 | /> 50 | { Methods.toggleFlash(); }} 52 | text="Flash" 53 | /> 54 | 55 | { 58 | Methods.takePicture(); 59 | }} 60 | > 61 | 62 | 63 | 64 | 65 | { Methods.pickImage(); }} 67 | text="Gallery" 68 | /> 69 | { Methods.flipCamera(); }} 71 | text="Flip" 72 | /> 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | const styles = StyleSheet.create({ 80 | scrollView: { 81 | backgroundColor: Colors.lighter, 82 | }, 83 | engine: { 84 | position: 'absolute', 85 | right: 0, 86 | }, 87 | body: { 88 | backgroundColor: Colors.white, 89 | }, 90 | sectionContainer: { 91 | marginTop: 32, 92 | paddingHorizontal: 24, 93 | }, 94 | sectionTitle: { 95 | fontSize: 24, 96 | fontWeight: '600', 97 | color: Colors.black, 98 | }, 99 | sectionDescription: { 100 | marginTop: 8, 101 | fontSize: 18, 102 | fontWeight: '400', 103 | color: Colors.dark, 104 | }, 105 | highlight: { 106 | fontWeight: '700', 107 | }, 108 | footer: { 109 | color: Colors.dark, 110 | fontSize: 12, 111 | fontWeight: '600', 112 | padding: 4, 113 | paddingRight: 12, 114 | textAlign: 'right', 115 | }, 116 | }); 117 | 118 | export default App; 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Native Fast Camera 📸

2 |

The most needed high-quality native camera for react native. A fast native camera controllable by react native components. 3 | 4 |

5 | Full Example 6 |

7 | 8 |

9 | 10 |

11 | 12 | ## Motivation 13 | I was looking for a flexible and fast camera component for react native, tried [react-native-camera](https://github.com/react-native-community/react-native-camera) and [react-native-camera-kit](https://github.com/wix/react-native-camera-kit) but was not satisfied with the performance. In a nutshell, this module is the product of months of research and experimentations with an aim to create a useful solution. 14 | 15 | ## Getting Started 16 | 17 | ### 1. Installation 18 | * with yarn: `yarn add react-native-fast-camera` 19 | * with npm: `npm install --save react-native-fast-camera` 20 | 21 | ### 2. Linking 22 | * 1. Copy `ios/FastCamera` folder to your `ios` folder. 23 | * 2. from Xcode Project navigator select `Add Files to "project name"` then select `ios/FastCamera`. 24 | 25 | ### 3. Permissions 26 | Add the following permission strings to your `info.plist` file, update the messages according to your needs. Otherwise your app will crash. 27 | ``` 28 | NSCameraUsageDescription 29 | We need to access your camera to take pictures. 30 | NSPhotoLibraryAddUsageDescription 31 | We need to access your photo library to save the captured pictures. 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```JSX 37 | import FastCamera, { Methods } from 'react-native-fast-camera'; 38 | ``` 39 | 40 | ```JSX 41 | { 43 | console.log("onSaveSuccess: ", imageUrl); 44 | }} 45 | onGalleryImage={imageUrl => { 46 | console.log("onGalleryImage: ", imageUrl); 47 | }} 48 | onFlashToggle={isflashOn => { 49 | console.log('flash info: ', isflashOn); 50 | }} 51 | > 52 | {/* here render your buttons to control the camera component */} 53 | 54 | ``` 55 | 56 | ## API 57 | | Property | Type | Description | 58 | |----------|:-------------:|----------| 59 | | onSaveSuccess | Callback | a callback triggered when the image was captured and saved successfully | 60 | | onGalleryImage | Callback | a callback triggered when the image was selected from the user photo library | 61 | | onFlashToggle | Callback | a callback triggered when the flash status change | 62 | | timer | Method | `Methods.timer()` a method to show timer UI | 63 | | toggleFlash | Method | `Methods.toggleFlash()` a method to toggle flash mode | 64 | | takePicture | Method | `Methods.takePicture()` a method to capture a picture | 65 | | pickImage | Method | `Methods.pickImage()` a method to pick an image from the photo library | 66 | | flipCamera | Method | `Methods.flipCamera()` a method to flip the camera face (front/back) | 67 | 68 | ## TODO 69 | - [x] Take a picture. 70 | - [x] Pick an image from photo library. 71 | - [x] Flip camera. 72 | - [x] Flash On/Off mode. 73 | - [x] Add timer. 74 | - [ ] **Add Android support**. 75 | - [ ] Add prop to set camera default face (front/back). 76 | - [ ] Export the iOS component as a pod module. 77 | 78 | ## ACKNOWLEDGEMENTS 79 | - [EVNCamera](https://github.com/zonghongyan/EVNCamera), used as the base for iOS part. 80 | -------------------------------------------------------------------------------- /ios/FastCamera/FastCamera.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "FastCamera.h" 3 | 4 | // import RCTEventDispatcher 5 | 6 | #import 7 | #import 8 | #import 9 | 10 | #import "EVNCameraController.h" 11 | 12 | @implementation FastCamera : UIView { 13 | 14 | RCTEventDispatcher *_eventDispatcher; 15 | 16 | EVNCameraController *_cameraController; 17 | 18 | } 19 | 20 | 21 | - (void)sendEvent:(UIEvent *)event { 22 | NSLog(@"event type: %@", event.type); 23 | } 24 | 25 | -(void)pickImage{ 26 | [_cameraController pickImage]; 27 | } 28 | -(void)toggleFlash{ 29 | [_cameraController toggleFlash]; 30 | } 31 | -(void)takePicture { 32 | [_cameraController takePicture]; 33 | } 34 | 35 | -(void)timer{ 36 | NSLog(@"step2"); 37 | // [_cameraController toggleTimer]; 38 | [_cameraController privateToggleTimer]; 39 | } 40 | 41 | - (void)flipCamera { 42 | NSLog(@"flipCamera called!!!"); 43 | [_cameraController flipMyCamera]; 44 | } 45 | 46 | 47 | - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher 48 | { 49 | if ((self = [super init])) { 50 | _eventDispatcher = eventDispatcher; 51 | 52 | // add initializations if needed here 53 | // CGRect frame = CGRectMake(0.0, 0.0, 200.0, 10.0); 54 | 55 | } 56 | 57 | return self; 58 | } 59 | 60 | - (void)layoutSubviews 61 | { 62 | [super layoutSubviews]; 63 | 64 | 65 | NSLog(@"uiview dimensions: %f %f",self.bounds.size.height, self.bounds.size.height); 66 | 67 | float deviceWidth = [UIScreen mainScreen].bounds.size.width; 68 | float deviceHeight = [UIScreen mainScreen].bounds.size.height; 69 | 70 | // self.bounds = CGRectMake(0, 0, 375, 667); 71 | 72 | // @important: this is needed so the cam take the whole device dimensions. 73 | self.bounds = [UIScreen mainScreen].bounds; 74 | 75 | 76 | _cameraController = [[EVNCameraController alloc] init]; 77 | _cameraController.cameraControllerDelegate = self; 78 | 79 | // @todo: make sure the bounds of the cam are respecting its parent view. 80 | // cameraController.view.bounds = self.bounds; 81 | 82 | [self addSubview: _cameraController.view]; 83 | 84 | } 85 | 86 | - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { 87 | } 88 | 89 | - (void)setNeedsFocusUpdate { 90 | } 91 | 92 | - (void)updateFocusIfNeeded { 93 | } 94 | 95 | // @todo: check if the folowing method needs to be removed 96 | - (NSArray *)supportedEvents { 97 | return @[@"onSaveSuccess", @"onGalleryImage", @"onFlashToggle"]; 98 | } 99 | 100 | -(void)flashDidFinishToggle:(NSDictionary *) status{ 101 | if(self.onFlashToggle){ 102 | self.onFlashToggle(status); 103 | } 104 | NSLog(@"flash toggled %@", status); 105 | } 106 | 107 | - (void)cameraDidFinishShootWithCameraImage:(UIImage *)cameraImage { 108 | NSLog(@"taken image is: %@", cameraImage); 109 | 110 | // Create paths to output images 111 | NSString *newFilePath = [NSString stringWithFormat:@"%s%@%s","Documents/", self.customFormattedDate, ".jpg"]; 112 | NSString *jpgPath = [NSHomeDirectory() stringByAppendingPathComponent:newFilePath]; 113 | NSString *folderPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/"]; 114 | 115 | [UIImageJPEGRepresentation(cameraImage, 1.0) writeToFile:jpgPath atomically:YES]; 116 | 117 | NSError *error; 118 | NSFileManager *fileMgr = [NSFileManager defaultManager]; 119 | 120 | 121 | // Point to Document directory 122 | NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; 123 | 124 | // Write out the contents of home directory to console 125 | NSArray *filesArray = [fileMgr contentsOfDirectoryAtPath:documentsDirectory error:&error]; 126 | NSLog(@"Documents directory: %@", filesArray); 127 | 128 | // handle filemanager errosp 129 | if(error != nil) { 130 | NSLog(@"Error in reading files: %@", [error localizedDescription]); 131 | return; 132 | } 133 | 134 | 135 | // check if the new file was saved successfully. 136 | BOOL fileExists = [fileMgr fileExistsAtPath:jpgPath]; 137 | if(fileExists){ 138 | NSLog(@"new file saved successfully %@", jpgPath); 139 | NSLog(@"%@", jpgPath); 140 | 141 | NSDictionary *imageInfo = @{ 142 | // @important: add `file://` prefix fo the file to work. 143 | @"image": [NSString stringWithFormat:@"%@%@", @"file://", jpgPath], 144 | }; 145 | 146 | // @important, the following check is needed to avoid the app crashing 147 | if(self.onSaveSuccess) { 148 | self.onSaveSuccess(imageInfo); 149 | } 150 | 151 | // [self sendEventWithName:@"onSaveSuccess" body:imageInfo]; 152 | 153 | } 154 | 155 | } 156 | 157 | 158 | - (void)didFinishPickingMediaWithInfo:(NSDictionary *)info 159 | { 160 | NSString *jpgPath = info[UIImagePickerControllerImageURL]; 161 | // @important: weird behavior when passed `jpgPath` js side received undefined and I 162 | // had to use a string formatter to fix that!. 163 | 164 | // [self sendEventWithName:@"onGalleryImage" body: [NSString stringWithFormat:@"%@", jpgPath]]; 165 | 166 | 167 | NSLog(@"gallery image -> RN:%@", jpgPath); 168 | 169 | NSDictionary *imageInfo = @{ 170 | //@"image": jpgPath, 171 | // @important: use `[NSString stringWithFormat:@"%@", jpgPath]` 172 | // to avoid `jpgPath` returning empty `null` for the js side 173 | @"image": [NSString stringWithFormat:@"%@", jpgPath], 174 | @"target": self.reactTag, //@important: needed for `_eventDispatcher` 175 | }; 176 | 177 | // [self setOnGalleryImage: @imageInfo]; 178 | // [_eventDispatcher sendInputEventWithName:@"onGalleryImage1" body:imageInfo]; 179 | 180 | // @important: we need to check if `self.onGalleryImage` 181 | // otherwise an exception is thrown. 182 | if(self.onGalleryImage){ 183 | NSLog(@"check if onGalleryImage is available"); 184 | self.onGalleryImage(imageInfo); 185 | } 186 | 187 | } 188 | 189 | // This method is called when an image has been chosen from the library or taken from the camera. 190 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 191 | { 192 | //You can retrieve the actual UIImage 193 | UIImage *image = [info valueForKey:UIImagePickerControllerOriginalImage]; 194 | //Or you can get the image url from AssetsLibrary 195 | NSURL *path = [info valueForKey:UIImagePickerControllerReferenceURL]; 196 | 197 | 198 | NSURL *imagePath = [info objectForKey:@"UIImagePickerControllerReferenceURL"]; 199 | NSString *imageName = [imagePath lastPathComponent]; 200 | NSLog(@"image url: %@ ", imageName); 201 | [picker dismissViewControllerAnimated:YES completion:nil]; 202 | 203 | } 204 | 205 | - (NSString *)customFormattedDate{ 206 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 207 | [formatter setDateFormat:@"dd-MM-yyyy_HH:mm"]; 208 | 209 | NSDate *currentDate = [NSDate date]; 210 | NSString *dateString = [formatter stringFromDate:currentDate]; 211 | return dateString; 212 | } 213 | 214 | //-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 215 | // CGFloat radius = 100.0; 216 | // CGRect frame = CGRectMake(-radius, -radius, 217 | // self.frame.size.width + radius, 218 | // self.frame.size.height + radius); 219 | // 220 | // NSLog(@"hitTest: %f %f %@", point.x, point.y, event); 221 | // 222 | // if (CGRectContainsPoint(frame, point)) { 223 | // return self; 224 | // } 225 | // return nil; 226 | //} 227 | 228 | 229 | 230 | @end 231 | -------------------------------------------------------------------------------- /ios/FastCamera/EVNCamera/EVNCameraController.m: -------------------------------------------------------------------------------- 1 | // 2 | // EVNCameraController.m 3 | // EVNCamera 4 | // 5 | // Created by developer on 2017/6/9. 6 | // Copyright © 2017 Ren Bian. All rights reserved. 7 | // 8 | 9 | #import "EVNCameraController.h" 10 | #import 11 | #import "CRCountdown.h" 12 | //#import 13 | #import 14 | 15 | #define kEVNScreenWidth [UIScreen mainScreen].bounds.size.width 16 | #define kEVNScreenHeight [UIScreen mainScreen].bounds.size.height 17 | 18 | @interface EVNCameraController () 22 | { 23 | BOOL isflashOn; // Whether the flash is turned on 24 | BOOL isTimerOn; 25 | BOOL isGalleryImage; 26 | 27 | NSDictionary *galleryImageInfo; 28 | 29 | CRCountdownUpdate update; 30 | CRCountdownCompletion completion; 31 | } 32 | 33 | /** 34 | * Capture device, usually front camera, rear camera 35 | */ 36 | @property (nonatomic, strong) AVCaptureDevice *device; 37 | 38 | /** 39 | * AVCaptureDeviceInput: input device, use AVCaptureDevice initialization 40 | */ 41 | @property (nonatomic, strong) AVCaptureDeviceInput *input; 42 | 43 | /** 44 | * Capture camera output 45 | */ 46 | @property (nonatomic, strong) AVCaptureStillImageOutput *imageOutPut; 47 | 48 | /** 49 | * Start capture camera 50 | */ 51 | @property (nonatomic, strong) AVCaptureSession *session; 52 | 53 | /** 54 | * Capture image layer in real time, image preview 55 | */ 56 | @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer; 57 | 58 | /** 59 | * White circle photo 60 | */ 61 | @property (nonatomic, strong) UIButton *photoButton; 62 | 63 | @property (nonatomic, strong) UIButton *uploadButton; 64 | 65 | @property (nonatomic, strong) UIButton *timerButton; 66 | @property (nonatomic, strong) UIButton *timerOff; 67 | @property (nonatomic, strong) UIButton *timer5s; 68 | @property (nonatomic, strong) UIButton *timer10s; 69 | @property (nonatomic, strong) UIButton *timerCounter; 70 | 71 | @property (nonatomic, strong) UIButton *flipCameraButton; 72 | 73 | @property (nonatomic, strong) UIButton *saveButton; 74 | /** 75 | * Turn on the flash button 76 | */ 77 | @property (nonatomic, strong) UIButton *flashButton; 78 | 79 | /** 80 | * Replay back to the screen after successful shooting 81 | */ 82 | @property (nonatomic, strong) UIImageView *imageView; 83 | 84 | /** 85 | * Focus green frame 86 | */ 87 | @property (nonatomic, strong) UIView *focusView; 88 | 89 | /** 90 | * Picture data taken 91 | */ 92 | @property (nonatomic, strong) UIImage *image; 93 | 94 | /** 95 | * Whether to enable camera permissions 96 | */ 97 | @property (nonatomic, assign) BOOL canUseCamera; 98 | 99 | /** 100 | * Cancel shooting 101 | */ 102 | @property (nonatomic, strong) UIButton *cancleButton; 103 | 104 | /** 105 | * Front rear camera switching 106 | */ 107 | @property (nonatomic, strong) UIButton *swapButton; 108 | 109 | /** 110 | * Retake 111 | */ 112 | @property (nonatomic, strong) UIButton *againTakePictureBtn; 113 | 114 | /** 115 | * Use pictures 116 | */ 117 | @property (nonatomic, strong) UIButton *usePictureBtn; 118 | 119 | @property (nonatomic, strong) UIImageView *tabImage; 120 | 121 | 122 | 123 | 124 | @end 125 | 126 | @implementation EVNCameraController 127 | 128 | - (void)viewDidLoad 129 | { 130 | [super viewDidLoad]; 131 | // Do any additional setup after loading the view. 132 | 133 | // if (self.isCanUseCamera) 134 | // { 135 | // [self customCamera]; 136 | // [self customCameraView]; 137 | // } 138 | // else 139 | // { 140 | // // @todo: ask the user for the camera permission. 141 | // NSLog(@"ask for camera permission"); 142 | // return; 143 | // } 144 | } 145 | 146 | - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration 147 | { 148 | NSLog(@"%ld", (long)toInterfaceOrientation); 149 | } 150 | 151 | - (void)didReceiveMemoryWarning { 152 | [super didReceiveMemoryWarning]; 153 | // Dispose of any resources that can be recreated. 154 | } 155 | 156 | - (void)viewDidAppear:(BOOL)animated 157 | { 158 | [super viewDidAppear:animated]; 159 | 160 | // MARK: Start autofocus for the first time 161 | [self focusAtPoint:CGPointMake(kEVNScreenWidth/2.0f, kEVNScreenHeight/2.0f)]; 162 | 163 | NSLog(@"viewDidAppear"); 164 | if(!self.isCanUseCamera) { 165 | NSLog(@"show custom ui asking for camera permission from settings"); 166 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Camera Permissions" message:@"We need camera permission so you can take and share your pictures. Please go to the settings to allow the app to access your camera: Settings - Privacy - Camera" preferredStyle:UIAlertControllerStyleAlert]; 167 | 168 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"No need" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) 169 | { 170 | [self cancleButtonAction]; 171 | } 172 | ]; 173 | 174 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Go to settings" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 175 | // Jump to set open permission 176 | NSURL * url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; 177 | if([[UIApplication sharedApplication] canOpenURL:url]) 178 | { 179 | [[UIApplication sharedApplication] openURL:url]; 180 | } 181 | }]; 182 | 183 | [alertController addAction:cancelAction]; 184 | [alertController addAction:okAction]; 185 | 186 | [self presentViewController:alertController animated:YES completion:nil]; 187 | 188 | } else { 189 | [self customCamera]; 190 | [self customCameraView]; 191 | } 192 | 193 | } 194 | 195 | /** 196 | * MARK: Whether to enable camera permissions 197 | @return return canUseCamera 198 | */ 199 | - (BOOL)isCanUseCamera 200 | { 201 | if (!_canUseCamera) 202 | { 203 | _canUseCamera = [self validateCanUseCamera]; 204 | } 205 | return _canUseCamera; 206 | } 207 | 208 | -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex 209 | { 210 | 211 | if (alertView.tag == 199) { 212 | if (buttonIndex == 1) { 213 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; 214 | } 215 | return; 216 | } 217 | } 218 | 219 | 220 | - (void) setButtonShadow:(UIButton *) button{ 221 | [button.layer setShadowOffset:CGSizeMake(0, 6)]; 222 | [button.layer setShadowColor:[[UIColor blackColor] CGColor]]; 223 | [button.layer setShadowOpacity: 1]; 224 | [button.layer setShadowRadius: 12]; 225 | } 226 | 227 | -(void)toggleFlash{ 228 | if ([_device lockForConfiguration:nil]) 229 | { 230 | if (isflashOn) 231 | { 232 | if ([_device isFlashModeSupported:AVCaptureFlashModeOff]) 233 | { 234 | [_device setFlashMode:AVCaptureFlashModeOff]; 235 | isflashOn = NO; 236 | NSLog(@"change flash icon to off"); 237 | } 238 | } 239 | else 240 | { 241 | if ([_device isFlashModeSupported:AVCaptureFlashModeOn]) 242 | { 243 | [_device setFlashMode:AVCaptureFlashModeOn]; 244 | isflashOn = YES; 245 | NSLog(@"change flash icon to on"); 246 | } 247 | } 248 | [_device unlockForConfiguration]; 249 | NSLog(@"flash is %s", isflashOn ? "true" : "false"); 250 | } 251 | NSLog(@"toggle flash called"); 252 | if ([self.cameraControllerDelegate respondsToSelector:@selector(flashDidFinishToggle:)]) 253 | { 254 | NSDictionary *info = @{ 255 | @"isflashOn": isflashOn ? @YES : @NO 256 | }; 257 | 258 | [self.cameraControllerDelegate flashDidFinishToggle:info]; 259 | } 260 | 261 | } 262 | -(void)takePicture { 263 | // @important: same logic as in `shatterCamera` method 264 | AVCaptureConnection * videoConnection = [self.imageOutPut connectionWithMediaType:AVMediaTypeVideo]; 265 | if (!videoConnection) 266 | { 267 | NSLog(@"Photo failure!"); 268 | return; 269 | } 270 | 271 | [self.imageOutPut captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error){ 272 | 273 | if (imageDataSampleBuffer == NULL) return; 274 | 275 | NSData * imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; 276 | self.image = [UIImage imageWithData:imageData]; 277 | 278 | UIImageWriteToSavedPhotosAlbum(self.image, self, @selector(cameraImage:didFinishSavingWithError:contextInfo:), NULL); 279 | 280 | 281 | [self.session stopRunning]; // stop session 282 | 283 | return; 284 | }]; 285 | } 286 | 287 | -(void)timer{ 288 | 289 | } 290 | 291 | - (void)flipMyCamera{ 292 | // @note: same logic used in `swapCamera` method. 293 | // use `dispatch_async` to make sure the code is executed in the main thread 294 | // and the animation is executed properly. 295 | dispatch_async(dispatch_get_main_queue(), ^{ 296 | NSUInteger cameraCount = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count]; 297 | if (cameraCount > 1) 298 | { 299 | NSError *error; 300 | 301 | CATransition *animation = [CATransition animation]; 302 | 303 | animation.duration = .5f; 304 | 305 | animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 306 | 307 | animation.type = @"oglFlip"; 308 | AVCaptureDevice *newCamera = nil; 309 | AVCaptureDeviceInput *newInput = nil; 310 | AVCaptureDevicePosition position = [[_input device] position]; 311 | if (position == AVCaptureDevicePositionFront) 312 | { 313 | newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack]; 314 | animation.subtype = kCATransitionFromLeft; 315 | } 316 | else 317 | { 318 | newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront]; 319 | animation.subtype = kCATransitionFromRight; 320 | } 321 | 322 | newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil]; 323 | [self.previewLayer addAnimation:animation forKey:nil]; 324 | if (newInput != nil) 325 | { 326 | [self.session beginConfiguration]; 327 | [self.session removeInput:_input]; 328 | if ([self.session canAddInput:newInput]) 329 | { 330 | [self.session addInput:newInput]; 331 | self.input = newInput; 332 | } 333 | else 334 | { 335 | [self.session addInput:self.input]; 336 | } 337 | 338 | [self.session commitConfiguration]; 339 | 340 | } 341 | else if (error) 342 | { 343 | NSLog(@"Switching camera failed, error = %@", error); 344 | } 345 | } 346 | }); 347 | } 348 | 349 | /** 350 | * MARK: Initialize the view required by the camera 351 | */ 352 | - (void)customCameraView 353 | { 354 | 355 | isTimerOn = NO; 356 | 357 | CGFloat device_width = [UIScreen mainScreen].bounds.size.width; 358 | 359 | _flashButton = [UIButton buttonWithType:UIButtonTypeCustom]; 360 | 361 | if(isflashOn) { 362 | [_flashButton setImage:[UIImage imageNamed:@"EVNCamera.bundle/flashOn.png"] forState:UIControlStateNormal]; 363 | } else { 364 | [_flashButton setImage:[UIImage imageNamed:@"EVNCamera.bundle/flashOff.png"] forState:UIControlStateNormal]; 365 | } 366 | 367 | [_flashButton addTarget:self action:@selector(flashOn:) forControlEvents:UIControlEventTouchUpInside]; 368 | [self.view addSubview:_flashButton]; 369 | 370 | _focusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)]; 371 | _focusView.layer.borderWidth = 0.51; 372 | _focusView.backgroundColor = [UIColor clearColor]; 373 | _focusView.layer.borderColor = [UIColor greenColor].CGColor; 374 | [self.view addSubview:_focusView]; 375 | _focusView.hidden = YES; 376 | 377 | _timerOff = [UIButton buttonWithType:UIButtonTypeCustom]; 378 | [_timerOff setTitle:@"Off" forState:UIControlStateNormal]; 379 | _timerOff.frame = CGRectMake(10, 40, 60, 30); 380 | [_timerOff.titleLabel setFont:[UIFont boldSystemFontOfSize: 15]]; 381 | [_timerOff setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 382 | [_timerOff setBackgroundColor: [UIColor colorWithRed:0.36 green:0.36 blue:0.41 alpha:1.0]]; 383 | [_timerOff addTarget:self action:@selector(turnOffTimer:) forControlEvents:UIControlEventTouchUpInside]; 384 | _timerOff.layer.cornerRadius = 5; 385 | _timerOff.clipsToBounds = YES; 386 | _timerOff.hidden = YES; 387 | [self.view addSubview: _timerOff]; 388 | 389 | _timer5s = [UIButton buttonWithType:UIButtonTypeCustom]; 390 | [_timer5s setTitle:@"5s" forState:UIControlStateNormal]; 391 | _timer5s.frame = CGRectMake(10 + 10 + 60, 40, 60, 30); 392 | [_timer5s.titleLabel setFont:[UIFont boldSystemFontOfSize: 15]]; 393 | [_timer5s setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 394 | [_timer5s setBackgroundColor: [UIColor colorWithRed:0.36 green:0.36 blue:0.41 alpha:1.0]]; 395 | [_timer5s addTarget:self action:@selector(setTimer5Sec:) forControlEvents:UIControlEventTouchUpInside]; 396 | _timer5s.layer.cornerRadius = 5; 397 | _timer5s.clipsToBounds = YES; 398 | _timer5s.hidden = YES; 399 | [self.view addSubview: _timer5s]; 400 | 401 | _timer10s = [UIButton buttonWithType:UIButtonTypeCustom]; 402 | [_timer10s setTitle:@"10s" forState:UIControlStateNormal]; 403 | _timer10s.frame = CGRectMake(10 + 10 + 60 + 10 + 60, 40, 60, 30); 404 | [_timer10s.titleLabel setFont:[UIFont boldSystemFontOfSize: 15]]; 405 | [_timer10s setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 406 | [_timer10s setBackgroundColor: [UIColor colorWithRed:0.36 green:0.36 blue:0.41 alpha:1.0]]; 407 | [_timer10s addTarget:self action:@selector(setTimer10Sec:) forControlEvents:UIControlEventTouchUpInside]; 408 | _timer10s.layer.cornerRadius = 5; 409 | _timer10s.clipsToBounds = YES; 410 | _timer10s.hidden = YES; 411 | [self.view addSubview: _timer10s]; 412 | 413 | _timerCounter = [UIButton buttonWithType:UIButtonTypeCustom]; 414 | [_timerCounter setTitle:@"0" forState:UIControlStateNormal]; 415 | _timerCounter.frame = CGRectMake(10, 40, 60, 30); 416 | [_timerCounter.titleLabel setFont:[UIFont boldSystemFontOfSize: 15]]; 417 | [_timerCounter setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 418 | [_timerCounter setBackgroundColor: [UIColor colorWithRed:0.36 green:0.36 blue:0.41 alpha:1.0]]; 419 | _timerCounter.layer.cornerRadius = 5; 420 | _timerCounter.clipsToBounds = YES; 421 | _timerCounter.hidden = YES; 422 | [self.view addSubview: _timerCounter]; 423 | 424 | self->update = ^(NSUInteger ticks) { 425 | // self.infoLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)ticks]; 426 | [_timerCounter setTitle:[NSString stringWithFormat:@"%lu", (unsigned long)ticks] forState:UIControlStateNormal]; 427 | NSLog(@"update timer"); 428 | }; 429 | 430 | self->completion = ^{ 431 | // self.infoLabel.hidden = YES; 432 | _timerCounter.hidden = YES; 433 | isTimerOn = NO; 434 | // do whatever else you want 435 | NSLog(@"timer finished"); 436 | 437 | // trigger camera button 438 | [self takePicture]; 439 | }; 440 | 441 | CGFloat DEVICE_WIDTH = [UIScreen mainScreen].bounds.size.width; 442 | CGFloat DEVICE_HEIGHT = [UIScreen mainScreen].bounds.size.height; 443 | 444 | NSLog(@"Device info %f %f", DEVICE_WIDTH, DEVICE_HEIGHT); 445 | 446 | 447 | UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(focusGesture:)]; 448 | [self.view addGestureRecognizer:tapGesture]; 449 | } 450 | 451 | 452 | - (AVCaptureSession *)extracted { 453 | return self.session; 454 | } 455 | 456 | /** 457 | * Custom camera 458 | */ 459 | - (void)customCamera 460 | { 461 | self.view.backgroundColor = [UIColor whiteColor]; 462 | 463 | self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; // use AVMediaTypeVideo Specify self.device Represents the video, which is initialized by default with the rear camera. 464 | 465 | self.input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:nil]; // Use device initialization input 466 | 467 | self.imageOutPut = [[AVCaptureStillImageOutput alloc] init]; 468 | 469 | self.session = [[AVCaptureSession alloc] init]; // Generate a session to combine input and output 470 | if ([self.session canSetSessionPreset:AVCaptureSessionPreset1280x720]) 471 | { 472 | self.session.sessionPreset = AVCaptureSessionPreset1280x720; 473 | } 474 | if ([self.session canAddInput:self.input]) 475 | { 476 | [[self extracted] addInput:self.input]; 477 | } 478 | 479 | if ([self.session canAddOutput:self.imageOutPut]) 480 | { 481 | [self.session addOutput:self.imageOutPut]; 482 | } 483 | 484 | // use self.session,Initialize the preview layer,self.session Driven input Collecting information,layer Responsible for rendering the image 485 | self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session]; 486 | self.previewLayer.frame = CGRectMake(0, 0, kEVNScreenWidth, kEVNScreenHeight); 487 | self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; 488 | [self.view.layer addSublayer:self.previewLayer]; 489 | 490 | [self.session startRunning]; // Start up 491 | if ([_device lockForConfiguration:nil]) 492 | { 493 | if ([_device isFlashModeSupported:AVCaptureFlashModeAuto]) 494 | { 495 | [_device setFlashMode:AVCaptureFlashModeAuto]; 496 | } 497 | if ([_device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) // Automatic white balance 498 | 499 | { 500 | [_device setWhiteBalanceMode:AVCaptureWhiteBalanceModeAutoWhiteBalance]; 501 | } 502 | [_device unlockForConfiguration]; 503 | } 504 | } 505 | 506 | 507 | /** 508 | * MARK: Turn off the flash 509 | @param sender Flash Button 510 | */ 511 | - (void)flashOn:(UIButton *)sender 512 | { 513 | if ([_device lockForConfiguration:nil]) 514 | { 515 | if (isflashOn) 516 | { 517 | if ([_device isFlashModeSupported:AVCaptureFlashModeOff]) 518 | { 519 | sender.selected = NO; 520 | sender.tintColor = [UIColor whiteColor]; 521 | [_device setFlashMode:AVCaptureFlashModeOff]; 522 | isflashOn = NO; 523 | [_flashButton setImage:[UIImage imageNamed:@"EVNCamera.bundle/flashOff.png"] forState:UIControlStateNormal]; 524 | NSLog(@"change flash icon to off"); 525 | } 526 | } 527 | else 528 | { 529 | if ([_device isFlashModeSupported:AVCaptureFlashModeOn]) 530 | { 531 | sender.selected = YES; 532 | sender.tintColor = [UIColor yellowColor]; 533 | [_device setFlashMode:AVCaptureFlashModeOn]; 534 | isflashOn = YES; 535 | [_flashButton setImage:[UIImage imageNamed:@"EVNCamera.bundle/flashOn.png"] forState:UIControlStateNormal]; 536 | NSLog(@"change flash icon to on"); 537 | 538 | } 539 | } 540 | [_device unlockForConfiguration]; 541 | NSLog(@"flash is %s", isflashOn ? "true" : "false"); 542 | } 543 | } 544 | 545 | /** 546 | * Switch camera, front/rear 547 | */ 548 | - (void)swapCamera:(UIButton *)sender 549 | { 550 | NSUInteger cameraCount = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count]; 551 | if (cameraCount > 1) 552 | { 553 | NSError *error; 554 | 555 | CATransition *animation = [CATransition animation]; 556 | 557 | animation.duration = .5f; 558 | 559 | animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 560 | 561 | animation.type = @"oglFlip"; 562 | AVCaptureDevice *newCamera = nil; 563 | AVCaptureDeviceInput *newInput = nil; 564 | AVCaptureDevicePosition position = [[_input device] position]; 565 | if (position == AVCaptureDevicePositionFront) 566 | { 567 | newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack]; 568 | animation.subtype = kCATransitionFromLeft; 569 | } 570 | else 571 | { 572 | newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront]; 573 | animation.subtype = kCATransitionFromRight; 574 | } 575 | 576 | newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil]; 577 | [self.previewLayer addAnimation:animation forKey:nil]; 578 | if (newInput != nil) 579 | { 580 | [self.session beginConfiguration]; 581 | [self.session removeInput:_input]; 582 | if ([self.session canAddInput:newInput]) 583 | { 584 | [self.session addInput:newInput]; 585 | self.input = newInput; 586 | } 587 | else 588 | { 589 | [self.session addInput:self.input]; 590 | } 591 | 592 | [self.session commitConfiguration]; 593 | 594 | } 595 | else if (error) 596 | { 597 | NSLog(@"Switching camera failed, error = %@", error); 598 | } 599 | } 600 | } 601 | 602 | 603 | /** 604 | * MARK: Camera switching operation 605 | @param position Camera position,front:AVCaptureDevicePositionFront end:AVCaptureDevicePositionBack 606 | @return AVCaptureDevice 607 | */ 608 | - (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position 609 | { 610 | NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 611 | for (AVCaptureDevice *device in devices ) 612 | { 613 | if (device.position == position) 614 | { 615 | return device; 616 | } 617 | } 618 | return nil; 619 | } 620 | 621 | /** 622 | * MARK: Focus gesture, get focus coordinates 623 | @param gesture tap 624 | */ 625 | - (void)focusGesture:(UITapGestureRecognizer*)gesture 626 | { 627 | CGPoint point = [gesture locationInView:gesture.view]; 628 | [self focusAtPoint:point]; 629 | } 630 | 631 | 632 | /** 633 | * MARK: Focus 634 | @param point Coordinate point of focus 635 | */ 636 | - (void)focusAtPoint:(CGPoint)point 637 | { 638 | CGSize size = self.view.bounds.size; 639 | CGPoint focusPoint = CGPointMake( point.y /size.height ,1-point.x/size.width ); 640 | NSError *error; 641 | if ([self.device lockForConfiguration:&error]) 642 | { 643 | 644 | if ([self.device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) 645 | { 646 | [self.device setFocusPointOfInterest:focusPoint]; 647 | [self.device setFocusMode:AVCaptureFocusModeAutoFocus]; 648 | } 649 | 650 | if ([self.device isExposureModeSupported:AVCaptureExposureModeAutoExpose ]) 651 | { 652 | [self.device setExposurePointOfInterest:focusPoint]; 653 | [self.device setExposureMode:AVCaptureExposureModeAutoExpose]; 654 | } 655 | 656 | [self.device unlockForConfiguration]; 657 | _focusView.center = point; 658 | _focusView.hidden = NO; 659 | [UIView animateWithDuration:0.3 animations:^{ 660 | _focusView.transform = CGAffineTransformMakeScale(1.25, 1.25); 661 | }completion:^(BOOL finished) { 662 | [UIView animateWithDuration:0.5 animations:^{ 663 | _focusView.transform = CGAffineTransformIdentity; 664 | } completion:^(BOOL finished) { 665 | _focusView.hidden = YES; 666 | }]; 667 | }]; 668 | } 669 | } 670 | 671 | /** 672 | * MARK: screenshot 673 | */ 674 | - (void)shutterCamera:(UIButton *)sender 675 | { 676 | AVCaptureConnection * videoConnection = [self.imageOutPut connectionWithMediaType:AVMediaTypeVideo]; 677 | if (!videoConnection) 678 | { 679 | NSLog(@"Photo failure!"); 680 | return; 681 | } 682 | 683 | [self.imageOutPut captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error){ 684 | 685 | if (imageDataSampleBuffer == NULL) return; 686 | 687 | NSData * imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; 688 | self.image = [UIImage imageWithData:imageData]; 689 | 690 | UIImageWriteToSavedPhotosAlbum(self.image, self, @selector(cameraImage:didFinishSavingWithError:contextInfo:), NULL); 691 | 692 | 693 | [self.session stopRunning]; // stop session 694 | return; 695 | }]; 696 | } 697 | 698 | - (void)saveImage:(UIButton *)sender 699 | { 700 | NSLog(@"image to save:"); 701 | if(isGalleryImage){ 702 | // self.image = [UIImage imageWithContentsOfFile: [galleryImageInfo objectForKey:@"UIImagePickerControllerReferenceURL"]]; 703 | 704 | UIAlertController *alertController = [UIAlertController 705 | alertControllerWithTitle:@"Warning" 706 | message:@"This Image Already exists on your gallery." 707 | preferredStyle:UIAlertControllerStyleAlert]; 708 | //We add buttons to the alert controller by creating UIAlertActions: 709 | UIAlertAction *actionOk = [UIAlertAction actionWithTitle:@"Ok" 710 | style:UIAlertActionStyleDefault 711 | handler:nil]; //You can use a block here to handle a press on this button 712 | [alertController addAction:actionOk]; 713 | [self presentViewController:alertController animated:YES completion:nil]; 714 | 715 | 716 | } else { 717 | // save the newly taken image. 718 | UIImageWriteToSavedPhotosAlbum(self.image, self, @selector(cameraImage:didFinishSavingAlert:contextInfo:), NULL); 719 | } 720 | } 721 | 722 | - (void)cameraImage:(UIImage *)cameraImage didFinishSavingAlert:(NSError *)error contextInfo:(NSDictionary *)contextInfo 723 | { 724 | if(error != NULL) 725 | { 726 | NSLog(@"error image!"); 727 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Warning" message:@"Failed to save image" preferredStyle:UIAlertControllerStyleAlert]; 728 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; 729 | [alertController addAction:okAction]; 730 | [self presentViewController:alertController animated:YES completion:nil]; 731 | } 732 | else 733 | { 734 | // image saved successfully. 735 | NSLog(@"image saved!"); 736 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Success" message:@"The image was saved successfully" preferredStyle:UIAlertControllerStyleAlert]; 737 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; 738 | [alertController addAction:okAction]; 739 | [self presentViewController:alertController animated:YES completion:nil]; 740 | } 741 | 742 | } 743 | 744 | - (void)pickImage 745 | { 746 | 747 | UIImagePickerController *pickerView = [[UIImagePickerController alloc] init]; 748 | // pickerView.allowsEditing = YES; 749 | pickerView.delegate = self; 750 | [pickerView setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]; 751 | // [self presentModalViewController:pickerView animated:YES]; 752 | 753 | NSLog(@"@pickImage method"); 754 | [self presentViewController:pickerView animated:YES completion:nil]; 755 | 756 | } 757 | 758 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { 759 | 760 | // get image selected when using `pickerView.allowsEditing = YES;` 761 | // UIImage *chosenImage = info[UIImagePickerControllerEditedImage]; 762 | UIImage *chosenImage = info[UIImagePickerControllerOriginalImage]; 763 | NSString *jpgPath = info[UIImagePickerControllerImageURL]; 764 | 765 | NSLog(@"image path: %@", jpgPath); 766 | 767 | 768 | isGalleryImage = YES; 769 | galleryImageInfo = info; 770 | 771 | NSMutableDictionary* imageInfoDict; 772 | imageInfoDict = [[NSMutableDictionary alloc] initWithCapacity:5]; 773 | [imageInfoDict setValue:jpgPath forKey:@"imagePath"]; 774 | [imageInfoDict setValue:jpgPath forKey:@"UIImagePickerControllerImageURL"]; 775 | 776 | // check if `RoaaCamera` has `didFinishPickingMediaWithInfo` method. 777 | // @note: don't call `self.cameraControllerDelegate didFinishPickingMediaWithInfo:info` 778 | // it is used only when the user needs clicks on upload and the js part, will push the share screen 779 | // if we call this then the js part will push the share screen 2times which is not desired!. 780 | 781 | [picker dismissViewControllerAnimated:YES completion:NULL]; 782 | 783 | if ([self.cameraControllerDelegate respondsToSelector:@selector(didFinishPickingMediaWithInfo:)]) 784 | { 785 | [self.cameraControllerDelegate didFinishPickingMediaWithInfo:imageInfoDict]; 786 | } 787 | 788 | 789 | return; 790 | 791 | } 792 | 793 | 794 | /** 795 | * MARK: Retake 796 | @param sender Retake button 797 | */ 798 | - (void)againTakePictureBtn:(UIButton *)sender 799 | { 800 | 801 | } 802 | 803 | /** 804 | * MARK: Retake 805 | @param sender sender 806 | */ 807 | - (void)usePictureBtn:(UIButton *)sender 808 | { 809 | 810 | if(isGalleryImage) { 811 | // the user picked an image from gallery 812 | NSLog(@"gallery image info: %@", galleryImageInfo); 813 | if ([self.cameraControllerDelegate respondsToSelector:@selector(didFinishPickingMediaWithInfo:)]) 814 | { 815 | [self.cameraControllerDelegate didFinishPickingMediaWithInfo: galleryImageInfo]; 816 | } 817 | 818 | } else { 819 | NSLog(@"image info:", self.image); 820 | // MARK: Save to album 821 | UIImageWriteToSavedPhotosAlbum(self.image, self, @selector(cameraImage:didFinishSavingWithError:contextInfo:), NULL); 822 | } 823 | [self cancleButtonAction]; 824 | } 825 | 826 | /** 827 | * MARK: Specify callback method 828 | @param cameraImage image 829 | @param error error 830 | @param contextInfo contextInfo 831 | */ 832 | - (void)cameraImage:(UIImage *)cameraImage didFinishSavingWithError:(NSError *)error contextInfo:(NSDictionary *)contextInfo 833 | { 834 | if(error != NULL) 835 | { 836 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"prompt" message:@"Failed to save image" preferredStyle:UIAlertControllerStyleAlert]; 837 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"determine" style:UIAlertActionStyleDefault handler:nil]; 838 | [alertController addAction:okAction]; 839 | UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; 840 | [rootViewController presentViewController:alertController animated:NO completion:nil]; 841 | } 842 | else 843 | { 844 | if ([self.cameraControllerDelegate respondsToSelector:@selector(cameraDidFinishShootWithCameraImage:)]) 845 | { 846 | [self.cameraControllerDelegate cameraDidFinishShootWithCameraImage:cameraImage]; 847 | } 848 | NSLog(@"image::: %@", cameraImage); 849 | // @important: the following line is needed to re-activate the cam when a user takes a camera shot. 850 | [self.session startRunning]; 851 | } 852 | } 853 | 854 | /** 855 | * MARK: Cancel shooting 856 | */ 857 | - (void)cancleButtonAction 858 | { 859 | [self.imageView removeFromSuperview]; 860 | [self dismissViewControllerAnimated:YES completion:nil]; 861 | 862 | 863 | [_swapButton sendActionsForControlEvents:UIControlEventTouchUpInside]; 864 | NSLog(@"cancel button clicked!"); 865 | } 866 | 867 | /** 868 | * MARK: Check camera permissions 869 | @return Whether to check camera permissions 870 | */ 871 | - (BOOL)validateCanUseCamera 872 | { 873 | AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 874 | if (authStatus == AVAuthorizationStatusDenied) 875 | { 876 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Please turn on camera permissions" message:@"Please go to the settings to allow the app to access your camera: Settings - Privacy - Camera" preferredStyle:UIAlertControllerStyleAlert]; 877 | 878 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"No need" style:UIAlertActionStyleCancel handler:nil]; 879 | UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"determine" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { 880 | // Jump to set open permission 881 | NSURL * url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; 882 | if([[UIApplication sharedApplication] canOpenURL:url]) 883 | { 884 | [[UIApplication sharedApplication] openURL:url]; 885 | } 886 | }]; 887 | [alertController addAction:cancelAction]; 888 | [alertController addAction:okAction]; 889 | 890 | UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; 891 | [rootViewController presentViewController:alertController animated:NO completion:nil]; 892 | return NO; 893 | } 894 | else 895 | { 896 | return YES; 897 | } 898 | } 899 | 900 | - (void)turnOffTimer:(UIButton *)sender 901 | { 902 | NSLog(@"turn off timer"); 903 | isTimerOn = NO; 904 | _timerOff.hidden = YES; 905 | _timer5s.hidden = YES; 906 | _timer10s.hidden = YES; 907 | } 908 | 909 | - (void)toggleTimer:(UIButton *)sender 910 | { 911 | if(isTimerOn) { 912 | isTimerOn = NO; 913 | [_timerOff sendActionsForControlEvents: UIControlEventTouchUpInside]; 914 | } else { 915 | isTimerOn = YES; 916 | 917 | _timerOff.hidden = NO; 918 | _timer5s.hidden = NO; 919 | _timer10s.hidden = NO; 920 | } 921 | NSLog(@"toggle timer"); 922 | } 923 | 924 | - (void)privateToggleTimer 925 | { 926 | if(isTimerOn) { 927 | isTimerOn = NO; 928 | 929 | NSLog(@"turn off timer"); 930 | isTimerOn = NO; 931 | 932 | dispatch_async(dispatch_get_main_queue(), ^{ 933 | // @important `dispatch_async` is needed because UI operations 934 | // must be performed in the main thread. 935 | _timerOff.hidden = YES; 936 | _timer5s.hidden = YES; 937 | _timer10s.hidden = YES; 938 | }); 939 | 940 | 941 | } else { 942 | isTimerOn = YES; 943 | 944 | dispatch_async(dispatch_get_main_queue(), ^{ 945 | // @important `dispatch_async` is needed because UI operations 946 | // must be performed in the main thread. 947 | _timerOff.hidden = NO; 948 | _timer5s.hidden = NO; 949 | _timer10s.hidden = NO; 950 | 951 | }); 952 | 953 | 954 | } 955 | NSLog(@"toggle timer"); 956 | } 957 | 958 | - (void)setTimer5Sec:(UIButton *)sender 959 | { 960 | NSLog(@"set timer 5sec"); 961 | [_timerCounter setTitle:@"5" forState:UIControlStateNormal]; 962 | 963 | CRCountdown *countdown = [[CRCountdown alloc] init]; 964 | [countdown startCountdownWithInterval:1.0 965 | ticks:5 966 | completion: self->completion 967 | update: self->update]; 968 | _timerCounter.hidden = NO; 969 | _timerOff.hidden = YES; 970 | _timer5s.hidden = YES; 971 | _timer10s.hidden = YES; 972 | 973 | } 974 | 975 | - (void)setTimer10Sec:(UIButton *)sender 976 | { 977 | NSLog(@"set timer 10sec"); 978 | [_timerCounter setTitle:@"10" forState:UIControlStateNormal]; 979 | 980 | CRCountdown *countdown2 = [[CRCountdown alloc] init]; 981 | [countdown2 startCountdownWithInterval:1.0 982 | ticks:10 983 | completion: self->completion 984 | update: self->update]; 985 | _timerCounter.hidden = NO; 986 | _timerOff.hidden = YES; 987 | _timer5s.hidden = YES; 988 | _timer10s.hidden = YES; 989 | 990 | } 991 | 992 | // Triggered when starting to shake 993 | - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event 994 | { 995 | NSLog(@"Start shaking"); 996 | } 997 | 998 | // Triggered when the end shakes 999 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event 1000 | { 1001 | NSLog(@"End of shaking"); 1002 | } 1003 | 1004 | // Triggered when shake is interrupted 1005 | - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event 1006 | { 1007 | NSLog(@"Cancel the shaking, stop the shaking"); 1008 | } 1009 | 1010 | - (void)dealloc 1011 | { 1012 | NSLog(@"%@, %s", NSStringFromClass([self class]), __func__); 1013 | } 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | @end 1021 | --------------------------------------------------------------------------------