In the previous chapter, you made use of Bazel to expand from command line programs, suitable for servers, and leaped into the mobile world through Android. To complete the work, we will create an equivalent client for iOS.
Note
In this chapter, we are going to be creating an iOS project using the native tools. However, since Xcode is only available on MacOS, you will only be able to build this chapter’s project on an MacOS machine.
Setup
Once again, we will be building off of our prior chapters. Verify that the prior chapter’s Bazel-generated files are eliminated; then copy the prior work:
$ cd chapter_09
chapter_09$ bazel clean
chapter_09$ ls
WORKSPACE client proto server
chapter_09$ cd ..
$ cp -rf chapter_09 chapter_10
$ cd chapter_10
chapter_10$
Since you have already set up Xcode on your machine to build the examples in the prior chapters, you should have everything that you need for developing your iOS application.
Workspace
Once again, we will add in dependencies to our WORKSPACE file to retrieve the rules required for building an iOS project.
In these particular changes, we are only explicitly calling into http_archive for the build_bazel_rules_apple; however, we are clearly getting multiple additional dependencies through the *_dependencies() functions. You have seen this in earlier chapters, but it is worth calling out since we will explicitly use rules from one of these additional packages (i.e., the build_bazel_rules_swift package).
Creating the iOS Client in Bazel
Similar to what you wrote under the Android example, we will start by creating a basic iOS application, ahead of actually employing any gRPC code. Along the way, we will explore a few of the fine points for building iOS applications under Bazel.
Let’s create a new directory for our iOS work. As expected, it will live within the client directory, adjacent to the Android and command line clients:
chapter_10$ cd client/echo_client/
chapter_10/client/echo_client$ mkdir ios
chapter_10/client/echo_client$ cd ios
chapter_10/client/echo_client/ios$
Within the client/echo_client/ios directory, create the MainViewController.swift file and add the following.
import UIKit
public class MainViewController : UIViewController {
private let textInput = UITextField()
private let sendButton = UIButton(type: UIButton.ButtonType.system)
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textInput.text = ""
receivedText.text = ""
}
@objc func send(sender: UIButton!) {
receivedText.text = textInput.text
}
}
Listing 10-2
Creating the MainViewController
Save the file to MainViewController.swift. As we did with the Android version of the client, we are creating a very simple local echo client, which just reflects the input text to an output upon clicking Send.
Note
Unlike the Android client, we are programmatically generating the UI through code, rather than creating an equivalent .storyboard file. Although the Bazel rules for iOS do support using .storyboard files, the standard tool for generating these files is Xcode itself (i.e., through the creation of a new project). For the sake of simplicity, we choose to forego using a .storyboard file, since the code for generating the UI is very straightforward.
In the Android example, we defined all of our application within a single file; here, we will create one more (i.e., the AppDelegate file) in order to follow iOS convention.
Within the client/echo_client/ios directory, create the AppDelegate.swift file and add the following.
import UIKit
@UIApplicationMain
class AppDelegate: NSObject, UIApplicationDelegate {
Given the work from prior chapters, nothing in the BUILD file should feel very foreign; you’ve simply loaded up a new set of rules and used them to create some build targets. In particular, swift_library should seem very familiar to similar instances in other languages. For the ios_application, many of the attributes are new, but also should make sense.
Building for iOS
Having set up our code and BUILD rules, let’s now execute a build. Let’s start with the EchoClient target:
INFO: Build completed successfully, 36 total actions
Once again, this should look very familiar from prior chapters.
However, let’s also perform a build on the Lib target. Although we have effectively successfully built this target as a dependency of EchoClient, it is worthwhile to have a discussion around having sufficient context to build a target.
ERROR: chapter_10/client/echo_client/ios/BUILD:4:1: Compiling Swift module client_echo_client_ios_Lib failed (Exit 1)
client/echo_client/ios/AppDelegate.swift:1:8: error: no such module 'UIKit'
import UIKit
^
Target //client/echo_client/ios:Lib failed to build
Use --verbose_failures to see the command lines of failed build steps.
INFO: Elapsed time: 4.094s, Critical Path: 0.15s
INFO: 0 processes.
FAILED: Build did NOT complete successfully
Although we have correctly set up the build targets, the build fails, with an error that should seem strange. After all, UIKit is a core library for iOS applications; it is always available when building an iOS application.
To understand what is going on, let us recall that Swift, as a language, is not bound to only building for iOS; you can create a native application via Swift for many platforms. Our initial specification within the BUILD file simply used swift_library; this alone gives no information as to what platform the Swift library should be built. Indeed, by default, it would be built for the MacOS platform (i.e., the system default).
In the case of ios_application, we are explicitly stating that this target should be cross-compiled for the iOS platform, pulling in the appropriate SDKs required to properly compile. Bazel has a property called configuration which encapsulates the environment’s information when performing a build. By default, the configuration from a build target will be applied to the dependencies. This is why you were able to successfully build all of EchoClient earlier, since the ios_application had the proper configuration and this applied to the dependencies.
To enable this for the swift_library alone, we can add some specification to the build command in order to properly execute.
Execute the following, which adds the directive –-apple_platform_type=ios to the command line:
INFO: Build completed successfully, 3 total actions
Having fully specified the platform, the build succeeds.
Note
You might recall from the prior chapter that we did not have to contend with specifying a platform explicitly. In the prior chapter, we used the rules android_binary and android_library; like ios_application, these were sufficient for specifying the targets’ build platform.
Note
Once again, an astute reader will notice the first INFO message, stating that the “analysis cache” has been discarded with the change in the build option. Recall that Bazel does a great deal of work to ensure the integrity of the build. In order to make sure that errors do not creep into the system, it is necessary to index the build products not only by what was built but also how it was built. Building a target with different options (e.g., platform, debug vs. opt, etc.) is basically equivalent to making a change to the target and all of its dependencies, at least for the purposes of caching the results.
Running the iOS Client in the Xcode Simulator
As we did for Android, we will take our initial build and run it on an iOS simulator to verify that it works.
We will first set up an iPhone simulator instance. Open Xcode, and navigate to Xcode ➤ Open Developer Tool ➤ Simulator.
Having started Simulator, you can now close down Xcode (this is similar to what we did in the last chapter with Android Studio).
In the Simulator application, let’s create a hardware device for an iPhone Xs with iOS 12.4.
This should create an instance of the iPhone Xs device simulator.
You are now ready to run the application on the simulator.
Executing the App on the Xcode Simulator
In the prior chapter, we were able to use bazel mobile-install <android_target> in order to build and install our application directly onto an Android simulator. Unfortunately, mobile-install only works for Android simulator instances; we can’t use exactly the same procedure for our iOS project. Attempting to do so would build but not actually execute the target on the simulator.
We can approximate the same effect of the mobile-install by using some Xcode commands directly. First, let’s make sure the build target is completely up to date. We will take particular note of the location of the generated .ipa file:
INFO: Build completed successfully, 1 total action
The .ipa file is actually a zipped directory. Although we cannot directly install the .ipa file onto the simulator, we can unzip it and install the underlying .app directory onto the simulator. Let’s first unzip the .ipa file to get its underlying contents:
Next, we will identify the active instance of the Xcode simulator by executing the following command. We are looking for the ID of the active instance:
Although it appears that there are two active instances, a close inspection reveals that the IDs are identical. Now, we are ready to install the application on the simulator. Execute the following command:
Within the application, you should see the following.
Click the EchoClient application. You should see the following.
For the basic application, you should also be able to tap on the input text box, write some text, and have it locally echo to the output label.
Congratulations! You have created and installed your first iOS application using Bazel!
Adding the gRPC to the iOS Application
Finally, as you have done in prior chapters, let’s add the gRPC functionality. As you might expect, the work required for iOS closely mimics what you did for the other clients.
Open proto/BUILD and add the following changes, highlighted in bold.
Save the file to proto/BUILD. The addition of the swift_proto_library and swift_grpc_library rules should come as no surprise to you.
Note
Both the swift_proto_library and the swift_grpc_library will auto-generate Swift module names as a combination of the path (relative to the root of the workspace) to the target and the target name itself. For example, in this case, the module name for transmission_object_swift_proto, whose relative path is /proto, will have a module name of proto_transmission_object_swift_proto.
Having created these new targets, let’s add them into our iOS Application. Open the client/echo_client/ios/BUILD file and add the following changes, highlighted in bold.
Now, we will add the Swift code for actually performing the gRPC call. Open client/echo_client/ios/MainViewController.swift and add the following, highlighted in bold.
import UIKit
import proto_transmission_object_proto
import proto_transceiver_proto
import proto_transceiver_swift_proto_grpc
<omitted for brevity>
@objc func send(sender: UIButton!) {
let client = Transceiver_TransceiverServiceClient(address: "localhost:1234", secure: false)
var transmissionObject = TransmissionObject_TransmissionObject()
Save the changes to client/echo_client/ios/MainViewController.swift.
Note
You might note that, unlike what you configured for the Android Studio simulator, we have reverted to using “localhost” for the address of our server. This is the proper address for getting to your development machine from within the iOS simulator.
Having added the gRPC functionality, let’s build and test our work:
INFO: Build completed successfully, 3 total actions
Note
An astute reader will notice that we have added the --apple_platform_type=ios directive to the command line. Earlier, the ios_application was sufficient to indicate how to compile swift_library target. In this case, since we are generating code for a protobuf dependency (which, as of this writing, might not yet properly handle the implicit toolchain transition), we explicitly specify the build option.
Having successfully built the app, let’s get the newest version installed on the simulator, repeating our earlier steps (with first removing our earlier unzipped directory):
INFO: Build completed successfully, 328 total actions
INFO: Build completed successfully, 328 total actions
2019/09/06 06:12:21 Spinning up the Echo Server in Go...
When you enter in the text within the iOS simulator and click Send, you should get a familiar response in the output.
Congratulations! You have successfully created an iOS application using Bazel, using gRPC to communicate.
Final Word
Throughout the course of this chapter, you expanded your Bazel knowledge for building applications for iOS. As you imagine, it would be very easy to build other applications for the MacOS family. As we saw in prior chapters, the given gRPC example is very much a toy example; however, it could be easily augmented to become vastly more interesting (e.g., a messaging application).