Inspired by Jason Lenny’s blog post at GitLab, How to publish iOS apps to the App Store with GitLab and fastlane, I wanted to see if we could improve on that and get test copies out to our team even faster.

Check out the 2 minute demo, and read on through the blog post to learn how to do this.

The right tool for the job

Jason’s proof of concept showed how to use fastlane to do an iOS beta. This relies on TestFlight to do the work of building and distributing the app. Since I had already played around with fastlane before, most of the time I spent getting the sample project to work was waiting on TestFlight to deploy my build. Sometimes it was as short as 5 minutes, sometimes as long as 30 minutes. It depends on how much of a backlog Apple’s servers have at the time.

For local development and testing, Xcode’s “run” button works fine. But there are a few cases where pulling some tools together with GitLab CI/CD can open up some new possibilities:

  • quickly switching back and forth between versions
  • small changes or where you don’t want to pull up the full development environment

Xcode wireless debugging allows you to install an app on devices on the network that are paired to your Mac, and ios-deploy makes this a simple command line you can bake into the CI process.

Note:

If you’re not on the same network as the Mac GitLab runner, then you may still need to put your build through TestFlight to get it on your device. If you do know of a way to use this faster approach without being on the same network, let me know in the comments!

But if you are on the same network, then why wait on TestFlight? After installing ios-deploy, you have everything you need to get faster to your next iteration. Your team doesn’t have to know Xcode to push a button and try a different version of the app on the review unit they’re using.

Try it yourself

Add ios-deploy as a global command:

npm install -g ios-deploy

(If you don’t have npm, try brew install node first)

If you’re playing along at home with Jason’s Flappy Bird example, and you’ve only got one device, you just need one CI variable and a couple new sections in your .gitlab-ci.yml file. Make sure you can build and install the app via Xcode first–it automatically handles things like downloading code signing certificates and getting your device provisioned.

Get the device ID

What you should see when you run ios-deploy

You’ll need the device’s ID so your runner knows which device to deploy to.

  • Use the ios-deploy -c command to check that your device shows up in the list and get the ID (that long hash at the front). If you don’t see it, check that you’re on the same network, and that you’ve configured the device to debug via the network in the Xcode Devices window.
  • Save the ID to a new CI/CD variable called DEVICE_ID
Where you plug in your device ID

Update CI config with review environment

Once you’ve got that set up, here’s the new .gitlab-ci.yml:

stages:
  - build
  - review
  - testflight

variables:
  LC_ALL: "en_US.UTF-8"
  LANG: "en_US.UTF-8"
  GIT_STRATEGY: clone

.build_template: &build_common
  stage: build
  script:
    - xcodebuild -destination='platform=ios'
  artifacts:
    paths:
    - ./build

build review:
  <<: *build_common
  environment:
    name: review/$CI_COMMIT_REF_NAME
  only:
    - branches
  except:
    - master

build production:
  <<: *build_common
  environment:
    name: production
  only:
    - master

review ipad:
  environment:
    name: ipad
  stage: review
  when: manual
  script:
    - echo "Deploy to iPad Pro"
    - cd build
    - ios-deploy --bundle Release-iphoneos/FlappyBird.app --id $DEVICE_ID
  only:
    - branches

testflight:
  stage: testflight
  script:
    - bundle install
    - bundle exec fastlane flappybuild
  when: manual
  artifacts:
    paths:
    - ./FlappyBird.ipa

What this does:

  • creates environments for your builds
    • One for master (production)
    • A review app for each branch aside from master (review/name-of-the-branch)
  • creates an environment for your iOS device

Deploy on demand

You can deploy from production or any review environment to the device.

In the environments page, you’ll find the review app you set up in each merge request, as well as the environment for each device you added as one of your review devices. Deploy any of those environments to your device by clicking the play button.

Screenshot of environments page with a deploy option visible

If you need to rollback to a previous version of the app to compare, click into that device’s review environment. There, you’ll see rollback buttons for all of the previous deployments.

Screenshot of iPad environment page with the mouse hovering over a rollback button

Additionally, the pipeline status at the top of the merge request will give you the same deployment button.

Screenshot of merge request pipeline with the review stage menu open

So, to recap:

master (production):

  • deploy to the device from the Operations -> Environments page

Others (review):

  • deploy from the environments page, or
  • deploy from the merge request, in the drop down for the review stage of the pipeline

You can still TestFlight when you need to from any of these areas. You’ll need to bump the version number in Info.plist in order to successfully deploy to TestFlight (you can’t send the same build number again). But, at least for interim builds, you can get by with just ios-deploy because it always rebuilds and reloads.

Tip: You can get rid of the when: manual from the review stage if you’re the only one working on the app. Then, the app will automatically be deployed on change. Be mindful of this if you’re working on multiple branches at a time.

The thrilling conclusion

With a click of a button, we can check our app on the iPad. We can deploy one of the in-development review apps, or the production version of the app, or deploy to TestFlight. To add another device, it’s just another job to define in the CI script.

Hope you liked it. If you can improve this even further, leave a comment! I’m especially interested to know of any way to deploy without being on the same network, or a better way to track different test devices in GitLab–the dynamic environments method works, but maybe doesn’t scale well past a few devices!

Stay tuned for a future post which will show this rapid development concept applied to a real-world application, an iOS app remotely controlling an infotainment system!

Need help with this, or more?

If you need help with your embedded/IoT device, or your app, or both, reach out to us! We’ve got plenty of experience with making embedded systems, and we know how to integrate them with custom apps for your phone.