This tutorial takes you through the process of creating a simple user interface with RoboVM using Interface Builder.
Interface Builder is part of Xcode and allows the creation of user interfaces for iOS apps via a WYSIWYG editor. User interfaces are usually defined in so called storyboards. A single storyboard can contain multiple scenes, each corresponding to a specific screen of your application. A scene is controlled by a view controller which interacts with views within the scene, such as buttons, labels, and text fields.
To accommodate different screen sizes, auto-layout can be used, which lets you define the relationships between UI elements, such as their size and relative spacing.
At the end of the tutorial you'll have an understanding of:
- How to create storyboard-based RoboVM iOS App
- How the project is structured
- How the app is started
- The basics of Xcode and the integrated Interface Builder
- The basics of auto-layouts
- How to wire up storyboards with Java code via actions and outlets
We will transform the boring template from this:
To this amazing application:
Our app will allow a user to enter her name and click on the Click Me!
button to see a nice personalized greeting.
Before you continue with this tutorial, we recommend going through the Getting Started Guide, which demonstrates how to properly setup the development environment of your choice.
The RoboVM plugins for IntelliJ IDEA and Eclipse both come with project templates that support Interface Builder.
- Go to File -> New -> Project....
- In the dialog, select RoboVM from the left hand list.
- Select RoboVM iOS Single View App and click Next.
- Fill out the basic information about the app and click Next.
- Specify the name of the project and its location, click Finish.
The RoboVM plugin will then create the project with support for Interface Builder.
- Go to File -> New -> RoboVM iOS Project
- In the dialog, specify the project's name.
- Select RoboVM iOS Single View App template selection.
- Fill out the basic information about the app and click Finish.
The RoboVM plugin will then create the project with support for Interface Builder.
The project structure of a storyboard-based project is similar to the structure of a code-only project, described in the Getting Started Guide.
The differences include:
- The
Info.plist.xml
file contains a key namedUIMainStoryboardFile
specifying the name of the storyboard file that will get loaded on startup. - App icons and launch images are located in
resources/Images.xcassets
. - Storyboard files and other Interface Builder artifacts are located in
resources/Base.lproj/
.
The project we created contains a storyboard file named Main.storyboard
. It is referenced by the Info.plist.xml
file under the UIMainStoryboardFile
key with value Main
.
The app consists of two classes: the main class and a view controller class. By using Interface Builder, both classes are significantly more simple compared to the code-only project from the getting started guide.
Open the Main.java
class:
package com.mycompany.myapp;
import org.robovm.apple.foundation.NSAutoreleasePool;
import org.robovm.apple.uikit.UIApplication;
import org.robovm.apple.uikit.UIApplicationDelegateAdapter;
import org.robovm.apple.uikit.UIApplicationLaunchOptions;
import org.robovm.apple.uikit.UIScreen;
import org.robovm.apple.uikit.UIWindow;
public class Main extends UIApplicationDelegateAdapter {
@Override
public boolean didFinishLaunching(UIApplication application, UIApplicationLaunchOptions launchOptions) {
return true;
}
public static void main(String[] args) {
try (NSAutoreleasePool pool = new NSAutoreleasePool()) {
UIApplication.main(args, null, Main.class);
}
}
}
The UIApplicationDelegate
implementation does not explicitly setup any controllers or UI, as opposed to what was demonstrated in the Getting Started Guide section. Instead, iOS will look into the Info.plist.xml
file, find the UIMainStoryboardFile
entry and automatically load the UI described by the file. On startup, the app will display the scene that is assigned to be the main entry point in the storyboard.
Every scene in a storyboard has a UIViewController
associated with it, responsible for implementing the logic of that scene. When the first scene of the main storyboard is loaded, iOS will also automatically instantiate the corresponding UIViewController
associated with the scene.
Open the file MyViewController.java
:
package com.mycompany.myapp;
import org.robovm.apple.uikit.UILabel;
import org.robovm.apple.uikit.UIViewController;
import org.robovm.objc.annotation.CustomClass;
import org.robovm.objc.annotation.IBAction;
import org.robovm.objc.annotation.IBOutlet;
@CustomClass("MyViewController") // [:3:]
public class MyViewController extends UIViewController {
private UILabel label;
private int clickCount;
@IBOutlet // [:1:]
public void setLabel(UILabel label) {
this.label = label;
}
@IBAction // [:2:]
private void clicked() {
label.setText("Click Nr. " + (++clickCount));
}
}
[:1:] Instead of setting up views like labels and buttons manually, UI views defined in the storyboard scene will be injected into the view controller when the scene is loaded. This is achieved through outlets, which are simple getters/setters annotated with @IBOutlet
, allowing us to get access to UI views from code.
[:2:] Instead of assigning listeners to views, like for receiving button clicks, the controller uses actions to be notified about such events. This is achieved by annotating controller methods with the @IBAction
annotation. This allows us to react to UI view events from code.
[:3:] When designing interfaces with Interface Builder, we visually connect views and their events with outlets and actions of the view controller. In order for this to work, Interface Builder needs to know the view controller's Objective-C class name. This can be indicated via the @CustomClass
annotation, which allows you to specify the name that Interface Builder will refer to the view controller.
Behind the scenes, RoboVM has to do additional work to make the bridge between Java code and Interface Builder work:
- RoboVM creates an Xcode project with references to the
Info.plist.xml
file, resources and view controllers. - RoboVM translates a controller's Java class, its actions, and outlets into Objective-C header files.
- RoboVM synchronizes changes between Xcode and the project bi-directionally, such as modifications to the
Info.plist.xml
file or changes to a view controller class.
All of this happens in the background. Every time you save changes to a relevant file in the project, RoboVM will update the Xcode or RoboVM project. You can catch a glimpse of this in the RoboVM console in Eclipse or IDEA:
Both the RoboVM plugin for Eclipse and the plugin for IntelliJ IDEA allow you to open storyboard files from within the IDE.
- In Eclipse/IDEA, double click the
Main.storyboard
file in theresources/Base.lproj/
folder OR - In Eclipse/IDEA, right clicking the RoboVM module/project and selecting Open Xcode Project from the context menu.
- The Navigator Area allows you to view the files within the Xcode project.
- The Editor Area displays the content of a file selected in the navigator area, using a content type specific UI.
- The Utility Area displays various properties of a selected object. An object can be a file selected in the navigator area or a UI view selected in the storyboard editor.
You can collapse the navigator and utility areas via the toolbar in the top right corner of Xcode
Select the storyboard file in the navigator. The editor area will display a WYSIWYG editor with the contents of the storyboard:
- The Outline View shows the contents of every scene in the storyboard in form of a tree. It is collapsible via the button in the bottom right corner.
- The Canvas allows you to visually modify the scenes.
- The Auto-layout Tools allow you to apply layout constraints to UI elements.
You can select view controllers, views, and constraints either by selecting them in the outline view or canvas.
You can delete elements by selecting them in either the outline view or the canvas and hitting the delete key.
You can zoom and pan the canvas using the mouse or touchpad. You can drag scenes around the canvas freely.
To add a new view to a scene in the storyboard, go to the Object Library at the bottom of the utility area:
You can find various standard UI elements in there like buttons, labels, and text fields. To add an element to the storyboard, simply drag it from the object library onto a scene in the canvas.
We want our user to be able to enter her name.
Add a text field to the scene:
- Type
text
into the search bar at the bottom of the object library. - Drag the text field from the object library onto the scene.
- Position the text field above the button, centering it horizontally by using the blue guides that appear.
- Resize the text field to the left and right edge of the scene, stopping when you hit the blue padding guides.
The result should look something like this:
Once an element is selected, you can view and modify its properties in the Inspector Pane in the utility area:
Via the inspector you can change view type dependent properties, such as font, color, and background images.
We want to indicate to our user that she has to enter her name in order for brilliant things to happen.
Modify the font size, add a placeholder text to the text field, and set a default text on the label:
- Select the text field in the outline view or in the canvas.
- In the inspector, set the
Placeholder
property toYour name here
. - In the inspector, click the T-icon on the font property and change the size to 24.
- Select the label in the outline view or in the canvas.
- In the inspector, set the text field beneath the
Text
combo box toI do not know you!
.
The result should look something like this:
One way of previewing the UI is to start the app on a simulator or device. Another way to quickly preview it is to use the Assistant Editor functionality:
- Click on the assistant editor button in the top right toolbar.
- In the breadcrumb part of the Assistant Editor, select the first crumb, then select
Preview (1)
, and finally select the storyboard file.
You will see something like this:
Within this preview area, there are a few actions you can now take to customize the views:
-
Click on the plus icon in the bottom left corner of the preview to add other device types.
-
Select a device and click the rotate button at the bottom of its frame to switch between portrait and landscape.
-
Delete a device by selecting it and then pressing the delete key.
NOTE: Previews are nice while iterating on your UI, but you should always test on a simulator or device. To run the app on a simulator or device, switch back to the Java IDE and run it from there. Launching your app via Xcode is not supported.
Our app doesn't look great in the preview because the constraints on the UI views are not what we want. Let's try to fix this.
Interface Builder helps you design UIs for different screen sizes via auto-layout. Auto-layout uses constraints to size and position UI views. A constraint puts the position and/or size of a UI view in relation to other elements or the scene itself.
Before applying constraints, let's define what our scene should look like:
- The
Click Me!
button should be centered vertically and horizontally. - The text field should be centered horizontally. It's left and right edges should be pinned to the left and right edges of the super view (which stretches the whole screen), with some spacing. It should also be placed relative to the top of the button, again with some spacing.
- The Label should be centered horizontally, it's left and right edges should be pinned to the left and right edges of the super view with some spacing, and it should also be placed relative to the top of the super view with some spacing.
Let's start by removing all constraints:
- In the canvas or outline view, select one of the views.
- In the canvas, click the
Resolve Auto Layout Issues
button. - From the context menu, select 'Clear Constraints' from the
All Views in My View Controller
category.
You should not see any constraints in the outline view anymore.
To center the Click Me!
button vertically and horizontally:
- Select the button in the canvas or outline view.
- In the canvas, click the 'Align' button.
- From the context menu, check the
Horizontal Center in Container
andVertical Center in Container
checkboxes.
4. Click the
Add 2 Constraints
button.
The canvas will now look like this:
The blue lines indicate that this UI view has a horizontal and vertical centering constraint applied.
The red and orange boxes tell us that there is something wrong with the auto-layout of this view. Let's find out what the problem is.
In the last step we applied new constraints to the button. This changed the button's location and size according to the auto-layout calculations. However, the location and size of our button on the canvas is different to the properties calculated by auto-layout. Interface Builder indicates this problem by:
- Showing orange guides and rectangles in the canvas for the element that has a problem.
- Showing a yellow, orange or red arrow icon in the outline view.
Click on the arrow icon:
Interface Builder tells us that the button is misplaced on the canvas. It expects a different position on the x-axis as well as a different width compared to what is currently visible in the canvas. Interface Builder can help you automatically resolve this issue.
Click on the triangle to the right of the issue:
Interface Builder suggests the most probable fix for the issue. The location and size of a UI view are stored in what is called a frame. The frame, as calculated by auto-layout, is different than the frame of the button on the canvas. Interface Builder suggests to update the frame of the button to match the frame as calculated by the auto-layout calculations.
Click the Fix Misplacement
button.
The red and orange outlines on the button are gone, and so is the arrow indicating auto-layout issues!
NOTE: When you place or otherwise modify a UI view, always check for orange/red outlines or guide lines as well as that little arrow in the outline view. Resolve auto-layout issues as early as possible!
Let's position the text field relative to the button's top edge and the super view's edges:
- Select the text field in the canvas or outline view.
- In the canvas, click the
Pin
button. - From the context menu, click the left, right and bottom edges.
- Click the
Add 3 Constraints
button.
The result will look like this:
The blue guide lines on the left and right represent our pinning constraints to the super view's edges, including margins. The little blue guide line between the text field and the button indicates that the text field will move with the button vertically, always keeping the set distance of 9 points.
Finally, let's layout the label:
- Select the label in the canvas or outline view.
- In the canvas, click the
Pin
button. - From the context menu, click the left, right and top edges.
- Enter
0
for the left and right edges. - Click the
Add 3 Constraints
button.
Since we pinned the label to the left and right margin of the super view, the label's frame as calculated by auto-layout is different from its frame on the canvas. Interface Builder indicates this via the small badges with the text +144
. We can resolve this just as we resolved the frame issue previously:
- Click the yellow arrow in the outline view.
- Click the triangle next to the issue.
- Select
Update Frame
, then clickFix Misplacement
.
Preview the app in the Assistant Editor with a few device types:
This looks like what we wanted!
My View Controller
is not really descriptive, so let's change it to something else:
- In Eclipse/IDEA, open
MyViewController.java
. - Rename the class to
GreetingsController
(make sure the file is renamed as well). - Change the
@CustomClass
annotation toGreetingsController
.
package com.mycompany.myapp;
import org.robovm.apple.uikit.UILabel;
import org.robovm.apple.uikit.UIViewController;
import org.robovm.objc.annotation.CustomClass;
import org.robovm.objc.annotation.IBAction;
import org.robovm.objc.annotation.IBOutlet;
@CustomClass("GreetingsController")
public class GreetingsController extends UIViewController {
private UILabel label;
private int clickCount;
@IBOutlet
public void setLabel(UILabel label) {
this.label = label;
}
@IBAction
private void clicked() {
label.setText("Click Nr. " + (++clickCount));
}
}
Once you save your changes, RoboVM will update the Xcode project accordingly.
NOTE: IntelliJ IDEA does not compile classes automatically when you change the source files. See the Getting Started Guide for IntelliJ IDEA on how to configure this behaviour!
Open Xcode, expand the Sources
and click on the GreetingsController.h
file. It will open in the editor area.
/**
* GreetingsController.h
*
* This file has been auto-generated by
* RoboVM's Interface Builder Integrator.
* DO NOT MODIFY!
*/
#import <UIKit/UIKit.h>
@interface GreetingsController : UIViewController
/**
* IBOutlet label
*/
@property (weak, nonatomic) IBOutlet UILabel *label;
/**
* IBAction clicked
* Selector: clicked
*/
-(IBAction) clicked;
@end
This file was auto-generated by RoboVM when you saved the modifications to the Java class. This is what allows Interface Builder to know about our controllers, their outlets, and their actions.
Since we changed the name of the view controller, we need to reassign it in our scene:
- In Xcode, open the
Main.storyboard
file by clicking on it in the navigator. - Select the view controller in the canvas by clicking the scene's title bar or in the outline.
- In the utility area, open the Identity Inspector.
- In the class field, select
GreetingsController
from the Class drop-down.
When iOS loads the application, it will first load the main storyboard, which in turn will load the scene and trigger the instantiation of the GreetingsController
class, both on the native and on the Java-side!
NOTE: The constructors of view controllers instantiated via storyboards are currently not called. To initialize your controllers, override the
viewDidLoad()
method and perform any setup there. You can follow the progress on this issue on the issue tracker
We will need access to the text field from within our Java-based view controller. To achieve this, we need to create an outlet on the view controller.
Add the following code to GreetingsController
:
private UITextField textField;
@IBOutlet
public void setTextField(UITextField textField) {
this.textField = textField;
}
To define an outlet in a Java-based view controller, create a setter (and optionally a getter) method following the Java bean naming conventions, annotated with @IBOutlet
. The setter method must take exactly one parameter, with the type of the UI view you want to assign to the outlet. In this case, the type is UITextField
, and likewise, the label is of type UILabel
. You can also specify a super type like UIView
. The optional getter must take no parameters and return a type according to the same typing rules as for the setter.
After you save the Java source file, RoboVM will update the corresponding Objective-C header file GreetingsController.h
:
/**
* IBOutlet textField
*/
@property (weak, nonatomic) IBOutlet UITextField *textField;
RoboVM added a property to the header file. The Java setter name is stripped of the set
prefix and used as the name for the outlet in the Objective-C header.
Let's assign the text field to this outlet:
- In Xcode, select the view controller in the outline view or canvas.
- Open the Connections Inspector.
- Click on the port next to the
textField
outlet and drag a line to the text field on the canvas or in the outline view.
The text field is now assigned to the outlet in our controller:
You can remove the assignment by clicking on the x
in the connections inspector.
Let's start the app and see if the outlet is assigned:
- In Eclipse/IDEA set a breakpoint in the
GreetingsController#setTextField
method. - Create a run configuration for the simulator or device.
- Start the run configuration in debug mode.
And indeed, the breakpoint is hit, and the passed in UITextField
is the one we defined in the storyboard:
Our Java-based view controller also has to react to events, such as a click on a button or when the content of a text field changes. We want to process changes to the text field value in addition to clicks on the Click Me!
button. To achieve this, we need to create an action on the view controller.
Add the following code to the GreetingsController
:
@IBAction
private void textChanged() {
label.setText("I see you typing!");
}
To define an action in a view controller, create a method annotated with @IBAction
. The method can have zero, one, or two parameters. The first optional parameter is the sender of the event, e.g. our UITextField
. The second optional parameter is a UIEvent
which holds additional data for the event. In our case we went for the zero parameter action.
After you save a Java source file, RoboVM will update the corresponding Objective-C header file GreetingsController.h
:
/**
* IBAction textChanged
* Selector: textChanged
*/
-(IBAction) textChanged;
RoboVM added a method to the header file. The Java method name is used as the name for the action.
Let's assign the text changed event of the text field to this action:
-
In Xcode, select the view controller in the outline view or canvas.
-
Open the connections inspector.
-
Click on the port next to the
textChanged
outlet and drag a line to the text field on the canvas or in the outline view.
The text changed event is now assigned to the action in our controller:
When the user types in the text field, the GreetingsController#textChanged()
method will be called:
Our app is almost complete. The only thing left to do is modify the controller code such that we assign a greeting text to the label when the Click Me!
button is pressed. The final code of GreetingsController
looks like this:
package com.mycompany.myapp;
import org.robovm.apple.uikit.UILabel;
import org.robovm.apple.uikit.UITextField;
import org.robovm.apple.uikit.UIViewController;
import org.robovm.objc.annotation.CustomClass;
import org.robovm.objc.annotation.IBAction;
import org.robovm.objc.annotation.IBOutlet;
@CustomClass("GreetingsController")
public class GreetingsController extends UIViewController {
private UILabel label;
private UITextField textField;
@IBOutlet
public void setTextField(UITextField textField) {
this.textField = textField;
}
@IBOutlet
public void setLabel(UILabel label) {
this.label = label;
}
@IBAction
private void clicked() {
String name = textField.getText();
if (name.isEmpty()) {
name = "Unknown";
}
label.setText("Hello " + name + "!");
}
@IBAction
private void textChanged() {
label.setText("I see you typing!");
}
}
Our application looks like this:
In this tutorial you got to know the basics of RoboVM's Interface Builder integration. Using storyboards results in very concise code. Modifying the UI visually is also a lot easier than coding it by hand. Finally, RoboVM allowed you to do all your coding in Java!
With your new knowledge you can now check out the other tutorials, as well as Apple's official documentation, to discover more!