© P.J. McNerney 2020
P. McNerneyBeginning Bazelhttps://doi.org/10.1007/978-1-4842-5194-2_10

10. Bazel and iOS

P. J. McNerney1 
(1)
Blackhawk, CO, USA
 

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.

Open the WORKSPACE file and add the following.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
skylib_version = "0.8.0"
http_archive(
    name = "bazel_skylib",
    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format(skylib_version, skylib_version),
)
git_repository(
    name ="build_bazel_rules_apple",
    commit="1445924a158a89ad634f562c84a600a3435ef8c2",
    remote="https://github.com/bazelbuild/rules_apple.git",
)
load(
    "@build_bazel_rules_apple//apple:repositories.bzl",
    "apple_rules_dependencies",
)
apple_rules_dependencies()
load(
    "@build_bazel_rules_swift//swift:repositories.bzl",
    "swift_rules_dependencies",
)
swift_rules_dependencies()
load(
    "@build_bazel_apple_support//lib:repositories.bzl",
    "apple_support_dependencies",
)
apple_support_dependencies()
<existing content omitted for brevity>
Listing 10-1

Modifying the WORKSPACE for the iOS rules

Save your changes to the WORKSPACE file.

Note

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)
    private let receivedText = UILabel()
    override public func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        textInput.placeholder = "Input text here"
        textInput.textColor = .black
        textInput.backgroundColor =.white
        textInput.isEnabled = true
        sendButton.setTitle("Send", for: UIControl.State.normal)
        sendButton.addTarget(self, action: #selector(send), for: .touchUpInside)
        sendButton.isEnabled = true
        receivedText.numberOfLines = 0
        receivedText.text = "Received text will show up here."
        receivedText.backgroundColor = .gray
        receivedText.textColor = .black
        let stackView = UIStackView(arrangedSubviews: [self.textInput, self.sendButton, self.receivedText])
        stackView.alignment = .fill
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        stackView.spacing = 10.0
        stackView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(stackView)
    }
    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 {
    var window: UIWindow?
    func application(
        _ application: UIApplication, didFinishLaunchingWithOptions
        launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()
        window?.rootViewController = MainViewController()
        return true
    }
}
Listing 10-3

Creating the basic AppDelegate

Save the file to AppDelegate.swift.

Finally, we will need to create an Info.plist file to define some of the basic attributes for our iOS application.

Within the client/echo_client/ios directory, create the Info.plist file and add the following.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UIRequiredDeviceCapabilities</key>
    <array>
        <string>arm64</string>
    </array>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
    </array>
</dict>
</plist>
Listing 10-4

Creating the Info.plist file

Save the file to Info.plist.

Within client/echo_client/ios directory, create a BUILD file and add the following to it.
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
    name = "Lib",
    srcs = [
        "AppDelegate.swift",
        "MainViewController.swift",
    ],
)
ios_application(
    name = "EchoClient",
    bundle_id = "com.beginning-bazel.echo-client",
    families = ["iphone"],
    infoplists = [":Info.plist"],
    minimum_os_version = "11.0",
    deps = [":Lib"],
)
Listing 10-5

Creating the BUILD file for the iOS project

Save the BUILD file.

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:
chapter_10/client/echo_client/ios$ bazel build :EchoClient
INFO: Analyzed target //client/echo_client/ios:EchoClient (19 packages loaded, 405 targets configured).
INFO: Found 1 target...
Target //client/echo_client/ios:EchoClient up-to-date:
  bazel-bin/client/echo_client/ios/EchoClient.ipa
INFO: Elapsed time: 9.965s, Critical Path: 9.48s
INFO: 10 processes: 7 darwin-sandbox, 2 local, 1 worker.
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.
chapter_10/client/echo_client/ios$ bazel build :Lib
INFO: Analyzed target //client/echo_client/ios:Lib (25 packages loaded, 854 targets configured).
INFO: Found 1 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:
chapter_10/client/echo_client/ios$ bazel build –-apple_platform_type=ios :Lib
INFO: Build option --apple_platform_type has changed, discarding analysis cache.
INFO: Analyzed target //client/echo_client/ios:Lib (3 packages loaded, 854 targets configured).
INFO: Found 1 target...
Target //client/echo_client/ios:Lib up-to-date:
  bazel-bin/client/echo_client/ios/Lib-Swift.h
  bazel-bin/client/echo_client/ios/client_echo_client_ios_Lib.swiftdoc
  bazel-bin/client/echo_client/ios/client_echo_client_ios_Lib.swiftmodule
  bazel-bin/client/echo_client/ios/libLib.a
INFO: Elapsed time: 4.139s, Critical Path: 3.72s
INFO: 2 processes: 1 darwin-sandbox, 1 worker.
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 XcodeOpen Developer ToolSimulator.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig1_HTML.jpg
Figure 10-1

Starting the 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.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig2_HTML.jpg
Figure 10-2

Selecting a Particular iOS Device to Simulate

This should create an instance of the iPhone Xs device simulator.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig3_HTML.jpg
Figure 10-3

iOS Simulator on Startup, for the Particular Device

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:
chapter_10/client/echo_client/ios$ bazel build :EchoClient
Starting local Bazel server and connecting to it...
INFO: Analyzed target //client/echo_client/ios:EchoClient (44 packages loaded, 1155 targets configured).
INFO: Found 1 target...
INFO: Deleting stale sandbox base /private/var/tmp/_bazel_pj/6ba16646dc915b8e018ad2c967b485b8/sandbox
Target //client/echo_client/ios:EchoClient up-to-date:
  bazel-bin/client/echo_client/ios/EchoClient.ipa
INFO: Elapsed time: 12.762s, Critical Path: 0.39s
INFO: 0 processes.
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:
chapter_10/client/echo_client/ios$ cd ../../..
chapter_10$ unzip bazel-bin/client/echo_client/ios/EchoClient.ipa
Archive:  bazel-bin/client/echo_client/ios/EchoClient.ipa
   creating: Payload/
   creating: Payload/EchoClient.app/
   creating: Payload/EchoClient.app/_CodeSignature/
 extracting: Payload/EchoClient.app/_CodeSignature/CodeResources
 extracting: Payload/EchoClient.app/EchoClient
   creating: Payload/EchoClient.app/Frameworks/
 extracting: Payload/EchoClient.app/Frameworks/libswiftCoreImage.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftObjectiveC.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftCore.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftCoreGraphics.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftUIKit.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftMetal.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftDispatch.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftos.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftCoreFoundation.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftDarwin.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftQuartzCore.dylib
 extracting: Payload/EchoClient.app/Frameworks/libswiftFoundation.dylib
 extracting: Payload/EchoClient.app/Info.plist
 extracting: Payload/EchoClient.app/PkgInfo
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:
chapter_10$ xcrun simctl list | grep Booted
 iPhone Xs (1DE26879-2844-4036-ABE2-A6B718A9CADA) (Booted)
 Phone: iPhone Xs (1DE26879-2844-4036-ABE2-A6B718A9CADA) (Booted)
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:
chapter_10$ xcrun simctl install 1DE26879-2844-4036-ABE2-A6B718A9CADA Payload/EchoClient.app
Within the application, you should see the following.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig4_HTML.jpg
Figure 10-4

Installed Application on the Simulator

Click the EchoClient application. You should see the following.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig5_HTML.jpg
Figure 10-5

Running the iOS EchoClient

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.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig6_HTML.jpg
Figure 10-6

Running a Simple Echo Test (Local only)

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.
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_grpc_library", "swift_proto_library")
proto_library(
    name = "transmission_object_proto",
    srcs = ["transmission_object.proto"],
)
<content omitted for brevity>
swift_proto_library(
    name = "transmission_object_swift_proto",
    deps = [":transmission_object_proto"],
    visibility = ["//client/echo_client:__subpackages__"],
)
swift_proto_library(
    name = "transceiver_swift_proto",
    deps = [":transceiver_proto"],
    visibility = ["//client/echo_client:__subpackages__"],
)
swift_grpc_library(
    name = "transceiver_swift_proto_grpc",
    srcs = [":transceiver_proto"],
    flavor = "client",
    deps = [":transceiver_swift_proto"],
    visibility = ["//client/echo_client:__subpackages__"],
)
Listing 10-6

Adding in the Swift protobuf rules

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.
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
    name = "Lib",
    srcs = [
        "AppDelegate.swift",
        "MainViewController.swift",
    ],
    deps = [
        "//proto:transmission_object_swift_proto",
        "//proto:transceiver_swift_proto",
        "//proto:transceiver_swift_proto_grpc",
    ],
)
ios_application(
    name = "EchoClient",
    bundle_id = "com.beginning-bazel.echo-client",
    families = ["iphone"],
    infoplists = [":Info.plist"],
    minimum_os_version = "11.0",
    deps = [":Lib"],
)
Listing 10-7

Adding in the Swift protobuf dependencies

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()
        transmissionObject.message = textInput.text ?? ""
        transmissionObject.value = 3.14
        var request = Transceiver_EchoRequest()
        request.fromClient = transmissionObject
        let response = try? client.echo(request)
        if let response = response {
            receivedText.text = response.fromServer.textFormatString()
        }
    }
Listing 10-8

Adding in the send/receive functionality

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:
chapter_10/client/echo_client/ios$ bazel build -–apple_platform_type=ios client/echo_client/ios:EchoClient
INFO: Analyzed target //client/echo_client/ios:EchoClient (16 packages loaded, 267 targets configured).
INFO: Found 1 target...
Target //client/echo_client/ios:EchoClient up-to-date:
  bazel-bin/client/echo_client/ios/EchoClient.ipa
INFO: Elapsed time: 1.851s, Critical Path: 0.09s
INFO: 0 processes.
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):
chapter_10/client/echo_client/ios$ cd ../../..
chapter_10/client/echo_client/ios$ rm -rf Payload
chapter_10$ unzip bazel-bin/client/echo_client/ios/EchoClient.ipa
chapter_10$ xcrun simctl install 1DE26879-2844-4036-ABE2-A6B718A9CADA Payload/EchoClient.app
Open the newly installed app within the iOS simulator. Now let’s run our server on the terminal:
chapter_10$ bazel run server/echo_server
Target //server/echo_server:echo_server up-to-date:
  bazel-bin/server/echo_server/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 14.009s, Critical Path: 12.36s
INFO: 324 processes: 324 darwin-sandbox.
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.
../images/481224_1_En_10_Chapter/481224_1_En_10_Fig7_HTML.jpg
Figure 10-7

Running the Echo Test Using gRPC

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.194.57