Preview, Playground, XCTest. Development without launching.

Share

By Dmitry Kurkin

One of the biggest parts of developing for iOS is building the UI. Checking the UI is hard to do without launching the app itself. Even with snapshot tests, aspects like animations and proper transitions are difficult to check. So, for every change in the code, we need to run the app and check if everything is correct. Missed some pixels? One more correction and one more launch.

Unfortunately, just launching it is not enough. We often need to proceed to the specific screen we want to check. In some cases, this may involve more than just a couple of taps. It could require interaction with “another user”, for example, or even triggering some processes on the server-side. Let’s try to list the steps that a developer follows to test a change in the code:

  1. Build the project
  2. Deploy on the device
  3. Reset all changes after last launch
  4. Launch the app
  5. Navigate to the screen
  6. Reproduce the issue

One more thing that we should mention here is the focus of the developer. While it builds, launches, or loads, it’s very easy to switch to something else – mail, messenger or comments in review. As a result, a short simple ticket can take the whole day instead of an hour.

How can we improve this process?

Build, deploy, and launch steps can be improved with hot reloads. This can involve a kind of WYSIWYG, where the changes are immediately applied without fully relaunching the application. In these cases, the steps of building, deploying, and launching become shorter and quicker, or even disappear altogether.

Mocked data and mocked services can bring us directly to any possible state of the app without the need for manual interaction with the interface. Simply put, this means that the steps of navigation and reproduction can be significantly reduced or avoided completely. Service stubs can also keep us safe from any side effects during further launches, meaning that the step of resetting changes is also no longer needed.

There are some alternative ways of development that could provide us with features that boost our performance:


1. Playground-driven development.

This idea was presented by Kickstarter on talk.objc.io. Here, instead of launching the app, we’re working in the Playground. All of our changes immediately appear on the right side of the Playground’s screen, meaning that there’s no need to deploy and relaunch. Sounds pretty cool! Below, you can see how this looks:


Starting with XCode 12.5 we have the option to “Import App Types”, which allows us to interact with our production code without any imports. Playgrounds are presented as something very close to WYSIWYG but changes still require some building steps. Despite this, updates are quicker to implement than through the method of having to relaunch the application in the simulator.

Unfortunately, the test environment is not accessible, so we couldn’t use any mocks from the test target. As a workaround, mocks can be moved to a separate framework and used as pseudo production code. In that case, Playgrounds can act as access points to some screens without having to go through any manual navigation. They also provide a good place for writing unit-test.

However, there are some drawbacks:
– Debugger is not accessible. When something goes wrong it’s hard to understand where the issue is. ‘Debug View Hierarchy’ and tools like Reveal also don’t work. So ‘print’ is the only rescue here.

– Playgrounds don’t have connections to the device, meaning we need to manage the proper size of the screen ourselves. Also, there’re no rotations or launches on the device itself.

– Playgrounds are still quite unstable. Some developers recommend regularly relaunching XCode to avoid some issues. An empty Playground causes cyclic crashes of XCode.


2. Swift UI Preview

Swift UI offers a new approach to building UI. Instead of using Interface Builder, UI are constructed by code and results checked in “Preview”. We can have different variants of our view simultaneously and all changes in the code immediately appear in the preview. Moreover, we’re not limited to SwiftUI. Preview can also show UIKit controls. With some wrappers we can check results in preview without having to launch the app:


In the case of SwiftUI, changes automatically appear in preview but updates that are part of UIKit require the building step. Despite this, the launching process looks faster than execution in the simulator.

Debug was disabled for previews but still could be used when attaching to the process. https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes

Previews couldn’t be used in test targets. It means the same situation occurs with mocks as we have in Playgrounds. If we could dismiss all the drawbacks of previews, they’d be a great tool to access any of our screens in any state. 

Here are the drawbacks of previews:
– Previews still have a lot of bugs. Sometimes it’s not clear how to reload them. This causes relaunches of XCode and rebuilds of the project. As a result, instead of speeding the process up, it costs us time.

– Attached debugger can’t help in initialisation steps. It comes too late to debug construction of views.

3. XCTest. Interactive test.

The XCTest can be referred to as a kind of unusual way of Unit-Testing. The main idea is simply running a very long XCTest, through which we can interact with our interface.



let expectation = XCTestExpectation()

wait(for: [expectation], timeout: 1000.0)

By default, XCTests are working over the host application. This means that it’s just some additional code that runs inside our application. This test can show some UI for a long time, allowing us to check how it looks and how it works. The result looks very similar to the usual launch, except starting directly from our screen:

These long tests would always be an issue for test runs on CI and they should be disabled for usual testing. But we could do it with condition and breakpoint:



if switcher {

    longWait(UINavigationController(

        rootViewController: Dependency.filter()

    ))
}

In cases where the project already has some test UI parts, for example, snapshot tests, it should be possible to create a specific screen with some mock-data. But instead of a static image we could interact with it and check the logic behind this UI.

We’re working inside the test target and could directly use our mocks without any tricks with the “pseudo-productions” mocks framework.

In cases where a project is dome without any tests at all, this is still very easy to take off. We can use more live data and the production part of the application before replacing it with mocks.

Here, we’re still launching the application. But in tests it’s usually blocked to avoid any interaction between tests and the production part. That’s why I would say that we’re not launching the application, but the test.

Of course, there’s no hot reload, so the build, deploy and launching steps take some time and updates are not as fast as they could be with a playground or with a preview.

Conclusion

The first two approaches are very promising and more modern, but at the same time very unstable. Hopefully, in the near future, they’ll receive more support in relation to debugging and testing, which should in turn reduce to the number of times it crashes. But, before that, interactive tests can provide a very good alternative. This is a stable solution that doesn’t bring any additional crashes to XCode. This simplifies much of the overall process.