during the webinar last week one of the questions we received was how to access and write platform-specific code with NativeScript, so we decided to create an iOS only application where you can use the entire native iOS api to create iOS-only app.
Here is how the app looks like:
It allows you to take pictures and to modify the picture properties like ISO, EXposure and WB(white balance) before capture the image.
You are going to see a lot of iOS specific APIs here. And frankly I'm so happy that you don't need to write those when creating cross-platform app with NativeScript. There is a lot of iOS specific knowledge required to understand this article, but you don't need to have that knowledge to write NativeScript apps.
This sample app is just to show you how you can access platform-specific code from NativeScript and to know that this code is there for you and you can use it when needed. It is very important to note that in the general case you will use only the NativeScripr cross-platform API and not those platform specific APIs.
Let the fun begin!
The source code is here:
You can clone the source code inside AppBuilder and install in on your iOS 8+ device immediately. To clone the app
- Open AppBuilder
- Choose "Create new Project"
- Choose "clone from existing repository" option
- Paste the git clone url of the repository. In this case the url is https://github.com/NativeScript/sample-iOS-CameraApp.git
- Click "Build" and follow the steps to deploy the app.
Below is an explanation of how the app is being build from one of the Technical Leads of NativeScript - Panayout Cankov.
Setting up EventLoop and MainViewController
The app.js is the entry point to you application and since we want to start up an UI we will have to run application loop ourselves. This is done by a call to UIApplicationMain and providing a UIApplicationDelegate.
This piece of code will create a new iOS class “AppDelegate” that extends UIResponder, and implements the UIApplicationDelegate protocol. The “extend” method receives two objects, the first defines new class members or overrides and the second describes additional attributes of the class.
In NativeScript for iOS we expose init methods as constructors so you can call “new UIWindow(…)” which maps to iOS [[UIWindow alloc] initWithFrame].
If you are familiar with the iOS API you would probably easily recognize the code.
The UIApplicationMain will start a message loop that would process system events and dispatch them through the UI layer.
So we have successfully set up the app delegate. We also need a root view controller.
Replace the “console.log” line from the AppDelegate with:
And then create a CameraViewController class just before the AppDelegate class:
The CameraViewController for now will simply add a white label at the top left of the app. Prepare, launch the app. You should be able to see it.
A few more things to point is the super instance property you can use to call the implementation of the method in the base class.
You can also call the implementation in the base class by obtaining it from the prototype of the constructor function of UIViewController:
But using super is way more convenient.
Adding Camera Layer
We do not need the label. Instead the CameraViewController have to start a capturing session, add a capturing device and display a preview of the camera. We end up with the following CameraViewController:
Prepare and launch the app. You may be asked for permissions from the app to access the camera. Now if you point the phone at something it should appear on the screen. If you are holding it on your desk like I did it would be blank black.
The CameraApp UI
We have the following images as a design so we will try to build something similar:
We will have 3 small circles as UI that when touched would bloom in a bigger radial menu. From them we will have a choice of several presets for white balance, exposure and ISO.
Implementing the Closed State of the Radial Menus
We will create a UIControl class that would encapsulate the functionality of the radial menus:
The new control have an initWithOptions initializer that invokes the super initWithRect, sets up colors and creates and adds a label as sub-view. We do not want to bloat up the code too much. The controls will be immutable except for the selection so all parameters will be passed in the options object.
You can see also three properties that would help us with the math during drawing, and then comes the drawRect override. In iOS UIView you can override drawRect and use the CoreGraphics drawing API to provide the visual representation for your control. We are using CGContextAddArc to create a small circle at the center of the control.
We will further extend these controls later but now let’s add three instances of them in the CameraViewController so we can check if everything is working fine. Add the createControls in the CameraViewController’s viewDidLoad:
And the additional method that would create the three radial menus:
Prepare and run the app. This will pretty much show the first screen from our design without the big red “capture” button.
Implementing the Arcs for the Radial Menus
Next we will create an arc view. We will use it to draw the open version of the radial menu. It will consist of several segments of a donut and a thicker red line that shows selection.
Our control will draw such segment of a donut in its drawRect by drawing an inner and outer arc sharing the same center of origin, with different radiuses starting and ending at the same angles. After we draw the arcs we will use “fill path” which will connect the starting and ending points of the two arcs:
This is the implementation of our ArcView class:
The expected options should have the following properties:
rInner – the inner radius as percent of the maximum radius that fits in the rect
rOuter – the outer radius as percent of the maximum radius that fits in the rect
segment – the angle, or length of the arc in radians
color – the background color used to fill the arc
Adding Arc Backgrounds in the Open State of the Radial Menu
In the DialSelector initWithOptions add:
self.optionsCount = 5;
self.segmentsCount = 8;
The first two fields describe the count of all “buttons” and the count of selectable “buttons”.
We need additional constants, declare them at the top of the file near the RELATIVE_DOT_SIZE:
GAP_EPSILON would be the angle inset for the buttons so that they don’t touch each other. Then implement the createButtons in the DialSelector as follows:
This will create instances of the ArcView class, add them as sub-views. Note the transform, it applies a scale of factor 0.2 so the petals will be contracted and also a rotation is applied so they spread in circle around the center point. In addition alpha is set to 0 so they won’t be visible. This is their default state.
When the control is touched the buttons will be animated to spread out. For that we have to track the touches. We are programing at control level so we won’t have to add or remove event targets but instead override the following methods:
beginTrackingWithTouchWithEvent will be invoked the control is touched. It returns true to indicate it would continuously observe the touch and continue will be invoked on drag. The continueTrackingWithTouchWithEvent will be invoked every time the touch is dragged. And finally endTrackingWithTouchWithEvent will be invoked when the touch is released. There is also cancelTrackingTouchWithEvent that we will ignore since our UI and interaction is fairly simple but you should generally implement it.
Also we will send the UIControlEventEditingDidBegin and UIControlEventEditingDidEnd events. We will need these along an upcoming UIControlEventValueChanged in the CameraViewController.
What is the bloom? It is another method that will handle the appearance and disappearance of the buttons:
The buttons inside bloom are animated to reach the scale and alpha settings provided from outside. bloom(1, 1) will show them and bloom(0.2, 0) will hide them. You can see that we will use the UIView.beginAnimationContext to record some changes on the UI. The rotation and scale of the buttons are recalculated. For fancier animation we also set different duration for each button. At the end UIView.commitAnimations() will collect the recorded changes and animate the buttons.
You can prepare and launch the app now. As a final touch on it we have to take care of the control’s hit area. Currently its hit are is as big as the bounding box of the opened state. Override point inside with event:
And you also need the constant:
Note that while the touch size is 0.25 and the actual drawing uses 0.2, the touch area will be a little bit bigger than the closed menu appears, courtesy of my fat thumbs. As for the math – the method simply calculates the distance from the center of the control to the touch point and returns true if that distance is less than a quarter of the radius of the maximum circle you can fit in the control’s frame.
Opening one Radial Menu Hides the Rest
When you press one of the menus the other two should disappear. As a bad practice I’ve seen people traversing the visual tree from their controls in similar situations where they would get the super-view of the DialSelector, iterate its sub-views and hide them. Instead we want to access only controls that we created or were given to us. This will improve the maintainability in future. So our CameraViewController will observe the DialSelectors in its createControls method:
And similar to our DialSelector’s bloom method it will animate the alpha of the closed menus when one of them is open:
If you prepare and run the app now you shall fail with:
2014-10-14 11:38:35.385 CameraApp[5764:2891504] -[UIViewController1 dialBeginEdit]: unrecognized selector sent to instance 0x15e0dbb0
Point one is achieved by setting the exposed methods in the second parameter of the UIControl.extend call for our DialSelector. Remember how we set class name for AppDelegate? Our MainViewControler does not need a name but will expose the two selectors ‘dialBeginEdit’ and ‘dialEndEdit’ to the native class:
The string – string pairs in the exposedMethods object links a selector with iOS types. The types in both are void return type: ‘v’, and an id as first parameter: ‘@’.
Point two is already fine as the DialSelectors lifetime is bound to the application lifetime.
Adding Views in the Buttons
The first of our radial menus display images, the second and the third display labels. We will need the following two factory functions in the CameraViewController:
The createLabels will build UILabels when provided an array with strings. The createImages will build UIImageViews when provided an array with image paths. The images should be located in the app/assets folder.
The MainViewController createControlls should look like this:
As you may note we have added five UI elements in the options for each DialSelector along with five values that would be used as selected value upon selection.
TODO: The minimum/maximum values in the ISO and exposure should be obtained from the device.
We also subscribe for the UIControlEventValueChangedevent on the control. We will use that to update the camera settings.
Let’s first display the options.
Add the following line in the DialSelector initWithOptions:
It will simply iterate all vies in the options and add them as subviews. It will also center the views over the dot and set the alpha to 0 to make them invisible.
We have a final touch to add in the DialSelector bloom method that would animate the views along the buttons. At the end, just before the commit-animation add:
We have to declare yet another constant:
The loop will iterate through all view items. An angle relative to the DialSelector center is calculated based on their index. And then they are positioned with a transformation using some trigonometry. The views will start from the top and rotate counter clock wise to the left and bottom.
Our DialSelector will have selectedIndex and selectedAngle properties that will be used to draw the red selection arc. We will consider them read-only for the outside world, and we do not expect other controls to set them. In the DialSelector initWithOptions method add the following code to initialize the selected index and angle to the middle/left item:
And in the DialSelector add the createSelection:
The red arc will now be added as sub-view. There are several points where we will interact with the alpha and the transformation of that arc. In the bloom method we will animate it first, just after the setAnimationCurve:
As you can see we will animate it to have scale factor of 0.2 for closed state and 1 for open and the alpha to be 0 for closed state and 1 for open. The rotation will come from the selectedAngle property.
If you run the project now you should be able to touch and hold the DialSelectors and the red arc would appear as if the left item is selected.
We have one more step, we should allow dragging over an item to change the selection. This happens in the DialSelector continueTrackingWithTouchWithEvent:
The method first calculates the location of the touch within the coordinate system of the control using locationInView. Then it calculates the dx - horizontal and dy - vertical distance from the touch point to the center of the control. Then, using the Pythagorean Theorem (c2 = a2 + b2), it calculates the distance from the center of the control to the touch point. If that distance is too small we will ignore the move. Otherwise the selection will change at the very moment you open the menu and the direction you point would be too random to consume.
If you’ve touched the dot and move far enough to the side the selection should change. Then we use Math.atan2 to calculate the angle from the center to the touch point, and recalculate the selectedIndex and selectedAngle properties. We have introduced a new property – value. It will contain an item from options.values that is located at the selectedIndex and would be extremely handy to use from the UIControlEventValueChanged event handlers. Finally we run new animation only for the selection arc just like we did recently in the bloom method. In addition we need to initialize the value in the initWithOptions so that it is correct at before any selection:
Run the application now. You should be able to touch one of the three DialSelectors, drag to the side and change the selection by rotating the selection arc.
Note that the sendActionsForControlEvents was commented out. We have not implemented the listeners in our CameraViewController. Uncomment that method and run the app again. Change the selection and the app will crash with unrecognized selector.
Add the updateCamera method in the CameraViewController:
Along with its definition in exposedMethods:
There are several key points here.
The lockForConfiguration and unlockForConfiguration will allow our app to apply changes on the device. Setting whiteBalanceMode is straight forward, our white balance values have auto property that we simply set on the whiteBalanceMode of the device. Setting white balance gains however is not that simple.
We have no use of completion handler so we simply pass null as second parameter.
We should not miss a call to updateCamera in CameraViewController’s viewDidLoad so that the camera is initialized with our settings at start. At the end of the viewDidLoad add:
Launch the app. You now have fully functional radial menus with selection integrated with the camera.
We have created controls with far more sophisticated shapes than the capture button. Here is the code that would create a button with the form of a big red dot with white stroke:
It looks fairly similar to the ArcView however it extends UIControl so we can consume its native events and instead of drawing two arcs it draws one arc to fill with red and a second one to draw the white outline.
We have yet to add it in the CameraViewController. Add a call to createShotButton in the viewDidLoad method:
And the following methods to actually create the button and add it as sub-view:
And list the exposed method in CameraViewController’s exposeMethods:
We will implement the takePicture later. Now let’s run the app. You should be able to see the red button.
Taking a Photo
To take a photo we will need an output for our camera input. Remember at the start we have created an AVCaptureSession session and an AVCaptureDeviceInput input? Well we need an output, lets add in the CameraViewController createCamera method an output of type AVCaptureStillImageOutput. This should be just before the this.session.startRunning() call:
Now we are ready to implement the takePhoto: